Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 11797] ファイルの概要

本ドキュメントは、Go言語コンパイラのコミット 896f0c61c8fbdaf4f6fa6007da8c03bbb818d85d に関する包括的な技術解説を提供します。このコミットは、Goプログラムにおける初期化ループ、特にfunc(関数)が関与するケースの診断機能の改善を目的としています。

コミット

commit 896f0c61c8fbdaf4f6fa6007da8c03bbb818d85d
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 10 23:10:45 2012 -0500

    gc: diagnose init loop involving func
    
    Fixes #2295.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5655057

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/896f0c61c8fbdaf4f6fa6007da8c03bbb818d85d

元コミット内容

このコミットは、Goコンパイラ(gc)において、関数が関与する初期化ループを診断する機能を追加します。具体的には、初期化ループが検出された際に、より詳細なエラーメッセージ(行番号を含む)を出力するように変更し、関連するテストケースを追加しています。

変更の背景

Go言語では、グローバル変数やパッケージレベルの変数は、プログラムの実行開始前に特定の順序で初期化されます。この初期化プロセス中に、変数間の依存関係が循環的になる「初期化ループ」が発生することがあります。例えば、変数 A が変数 B に依存し、変数 B が変数 A に依存するような場合です。このようなループは、コンパイラが初期化の順序を決定できないため、コンパイルエラーとなります。

以前のGoコンパイラでは、このような初期化ループが検出された際のエラーメッセージが不十分であり、特に匿名関数(func)が初期化ループに関与している場合に、問題の特定が困難でした。コミットメッセージにある Fixes #2295 は、この特定の問題(Go issue 2295)を解決することを指しています。この問題は、関数変数の初期化ループに関する診断の改善を求めるものでした。

前提知識の解説

Go言語の初期化順序

Goプログラムの初期化は以下の順序で行われます。

  1. インポートされたパッケージの初期化: 各パッケージは、そのパッケージがインポートする他のパッケージがすべて初期化された後に初期化されます。
  2. パッケージレベルの変数の初期化: 各パッケージ内で宣言されたグローバル変数やパッケージレベルの変数は、宣言順に初期化されます。ただし、変数が他の変数に依存している場合、依存先の変数が先に初期化されます。
  3. init() 関数の実行: 各パッケージに複数定義できる init() 関数は、パッケージレベルの変数がすべて初期化された後に実行されます。init() 関数は引数を取らず、戻り値もありません。

初期化ループ

初期化ループは、上記の初期化順序において、変数間の依存関係が循環的になった場合に発生します。例えば、以下のGoコードは初期化ループの典型的な例です。

package main

var a = b + 1
var b = a + 1

func main() {
    // ...
}

この場合、ab に依存し、ba に依存するため、コンパイラはどちらを先に初期化すべきか判断できません。

Goコンパイラの構造(gcsinit.c

Goコンパイラ(gc)は、Goソースコードを機械語に変換するツールチェーンの一部です。src/cmd/gc/sinit.c は、Goコンパイラの初期のC言語で書かれた部分であり、静的初期化(static initialization)の処理を担当していました。これには、グローバル変数の初期化順序の決定や、init() 関数の呼び出し順序の管理などが含まれます。現代のGoコンパイラでは、これらの機能はGo言語自体で再実装され、sinit.go のようなファイルに相当するロジックが存在します。

init1init2 は、Goコンパイラの内部で初期化処理を管理するための関数であると推測されます。init1 は初期化処理の第一段階、init2 は第二段階、あるいは異なる種類の初期化処理を担当している可能性があります。このコミットでは、init1 から init2 への呼び出しの変更が見られ、これは初期化ループの診断ロジックの改善に関連しています。

技術的詳細

このコミットの主要な目的は、Goコンパイラが初期化ループを検出した際のエラーメッセージを改善し、特に匿名関数が関与するケースでの診断を容易にすることです。

変更点の一つは、src/cmd/gc/sinit.c 内の print("initialization loop:\\n"); という汎用的なエラーメッセージを、print("%L: initialization loop:\\n", n->lineno); に変更している点です。これにより、エラーが発生したソースコードの行番号がメッセージに含まれるようになり、開発者は問題の箇所を迅速に特定できるようになります。

もう一つの重要な変更は、init1(n->defn->right, out); の呼び出しが init2(n->defn->right, out); に変更されている点です。これは、初期化ループの検出ロジックにおいて、特定のノード(n->defn->right)の初期化処理を init1 ではなく init2 に委ねることを意味します。この変更は、関数が関与する複雑な初期化依存関係をより正確に解析し、ループを検出するためのコンパイラ内部のロジックの調整であると考えられます。init1init2 の具体的な役割はコンパイラの内部実装に依存しますが、この変更は初期化グラフの走査や依存関係の解決方法に影響を与え、より堅牢なループ検出を可能にしていると推測されます。

追加されたテストケース test/fixedbugs/bug413.go は、この変更が意図通りに機能することを確認するためのものです。このテストケースは、匿名関数がグローバル変数に依存し、そのグローバル変数が匿名関数に依存するという、まさに初期化ループを形成するシナリオを再現しています。

var i = func() int {a := f(i); return a}()

この行では、グローバル変数 i が匿名関数によって初期化され、その匿名関数内で f(i) が呼び出されています。つまり、i の初期化には i 自身が必要となるため、初期化ループが発生します。このテストケースは、コンパイラがこのループを正しく検出し、「initialization loop」というエラーメッセージを出力することを期待しています。

コアとなるコードの変更箇所

src/cmd/gc/sinit.c

--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -65,7 +65,7 @@ init1(Node *n, NodeList **out)
 		if(nerrors > 0)
 			errorexit();
 
-		print("initialization loop:\n");
+		print("%L: initialization loop:\n", n->lineno);
 		for(l=initlist;; l=l->next) {
 			if(l->next == nil)
 				break;
@@ -106,7 +106,7 @@ init1(Node *n, NodeList **out)
 			break;
 		*/
 		if(1) {
-			init1(n->defn->right, out);
+			init2(n->defn->right, out);
 			if(debug['j'])
 				print("%S\n", n->sym);
 			if(!staticinit(n, out)) {

test/fixedbugs/bug413.go

--- /dev/null
+++ b/test/fixedbugs/bug413.go
@@ -0,0 +1,11 @@
+// errchk $G $D/$F.go
+
+// Copyright 2012 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package p
+
+func f(i int) int { return i }
+
+var i = func() int {a := f(i); return a}()  // ERROR "initialization loop"
\ No newline at end of file

コアとなるコードの解説

src/cmd/gc/sinit.c の変更点

  1. エラーメッセージの改善: print("initialization loop:\\n"); から print("%L: initialization loop:\\n", n->lineno); への変更は、エラーメッセージに行番号 (n->lineno) を含めるようにしています。これにより、コンパイラが初期化ループを検出した際に、どのソースコードの行で問題が発生しているのかを正確に報告できるようになり、デバッグの効率が向上します。%L はGoコンパイラ内部のフォーマット指定子で、ノードの行番号を出力するために使用されます。

  2. 初期化処理の委譲: init1(n->defn->right, out); から init2(n->defn->right, out); への変更は、初期化ループの検出ロジックにおける重要な調整です。n->defn->right は、おそらく初期化されるべき式の右辺を表す抽象構文木(AST)のノードを指しています。このノードの初期化処理を init1 から init2 に切り替えることで、コンパイラは関数が関与する初期化依存関係をより適切に処理し、循環参照を正確に特定できるようになります。これは、init1init2 がそれぞれ異なる初期化フェーズや、異なる種類の依存関係解決ロジックを担当していることを示唆しています。

test/fixedbugs/bug413.go の追加

この新しいテストファイルは、Goコンパイラが関数を含む初期化ループを正しく診断できることを検証するために追加されました。

  • // errchk $G $D/$F.go: この行は、Goテストフレームワークに対する指示であり、このファイルがコンパイルエラーを発生させることを期待していることを示します。$G はGoコンパイラ、$D/$F.go は現在のテストファイルのパスを指します。
  • var i = func() int {a := f(i); return a}(): この行がテストの核心です。
    • var i: パッケージレベルの変数 i を宣言しています。
    • func() int { ... }(): これは匿名関数を定義し、その場で実行しています。この匿名関数の戻り値が変数 i の初期値となります。
    • a := f(i): 匿名関数内で、関数 f が呼び出され、その引数として変数 i 自身が渡されています。 この構造により、変数 i の初期化には、i 自身の値が必要となるため、初期化ループが発生します。コンパイラはこれを検出し、// ERROR "initialization loop" で示されているように、「initialization loop」というエラーメッセージを出力することが期待されます。

このテストケースの追加により、コンパイラが特定の種類の初期化ループ(特に匿名関数が関与するもの)を正確に診断できるようになったことが保証されます。

関連リンク

  • Go言語の初期化に関する公式ドキュメントや仕様: Go言語の仕様書(Language Specification)の「Program initialization and execution」セクションが該当します。
  • Goコンパイラのソースコードリポジトリ: src/cmd/compile ディレクトリ以下に、現在のGoコンパイラのソースコードがあります。

参考にした情報源リンク

  • Go言語の初期化ループに関するStack Overflowの議論やブログ記事
  • Go言語のissueトラッカー(golang/go リポジトリのissue #2295に関連する議論)
  • Goコンパイラの内部構造に関する技術ブログやドキュメント