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

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

このコミットは、Goコンパイラ(cmd/gc)におけるトップレベルのブランク変数(_)の初期化順序の計算に関するバグを修正するものです。具体的には、トップレベルで宣言されたブランク識別子への代入が、他の変数や関数の初期化順序に正しく影響を与えないために発生していた、nil関数呼び出しによるパニックを解決します。

コミット

commit 880d86976454d228d4b2d4cbfd264873a8bcab31
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Jun 11 22:21:51 2013 +0200

    cmd/gc: compute initialization order for top-level blank vars too.
    
    Fixes #5244.
    
    R=golang-dev, rsc, iant, r, daniel.morsing
    CC=golang-dev
    https://golang.org/cl/8601044

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

https://github.com/golang/go/commit/880d86976454d228d4b2d4cbfd264873a8bcab31

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)がトップレベルのブランク変数(_)に対しても初期化順序を計算するように変更します。これにより、Go言語のIssue 5244で報告されたバグが修正されます。このバグは、トップレベルのブランク識別子への代入が、その代入に含まれる式が依存する他の変数や関数の初期化順序に正しく考慮されないために発生していました。

変更の背景

Go言語では、パッケージレベルの変数は宣言順に初期化されるという厳格なルールがあります。しかし、Issue 5244(test/fixedbugs/issue5244.goで再現される)は、この初期化順序の計算に欠陥があることを示していました。

問題のコードは以下のようになります。

package main

var f = func() int { return 1 }
var _ = f() + g()
var g = func() int { return 2 }

func main() {}

このコードでは、f_(ブランク変数)、g の順に宣言されています。Goの初期化順序のルールに従えば、f が初期化され、次に _ = f() + g() が評価され、最後に g が初期化されるはずです。

しかし、バグのあるコンパイラでは、var _ = f() + g() の行が評価される際に、g がまだ初期化されていない状態(gnil 関数ポインタである状態)で g() が呼び出されてしまい、パニックが発生していました。これは、コンパイラがトップレベルのブランク変数への代入を、初期化順序の依存関係として正しく追跡していなかったためです。

このコミットは、この初期化順序の計算ロジックを修正し、トップレベルのブランク変数への代入も初期化グラフに含めることで、f()g() が正しく初期化された後に f() + g() が評価されるようにします。

前提知識の解説

Go言語の初期化順序

Go言語では、プログラムの実行前にパッケージレベルの変数が初期化されます。初期化の順序は以下のルールに従います。

  1. パッケージの依存関係: インポートされたパッケージは、そのパッケージを使用するパッケージよりも先に初期化されます。循環依存は許可されません。
  2. パッケージ内の宣言順: 同じパッケージ内では、変数は宣言された順序で初期化されます。
  3. 初期化式: 変数の初期化式は、その変数が初期化されるときに評価されます。初期化式が他の変数に依存する場合、その依存する変数は先に初期化されます。
  4. init 関数: 各パッケージは複数の init 関数を持つことができ、これらはパッケージ内のすべての変数が初期化された後に、宣言順に実行されます。

このコミットの文脈では、特に「パッケージ内の宣言順」と「初期化式が他の変数に依存する場合の順序」が重要になります。

ブランク識別子 (_)

Go言語のブランク識別子(_)は、値が不要な場合に使用される特別な識別子です。例えば、関数の戻り値の一部を無視したり、インポートしたパッケージの副作用だけを利用したい場合に利用されます。

// 戻り値を無視
_, err := someFunction()

// パッケージの副作用のみを利用(init関数など)
import _ "net/http/pprof"

このコミットで問題となっているのは、トップレベルでブランク識別子に変数を代入するケースです。

var _ = someFunction()

このような宣言は、someFunction() の評価をトリガーしますが、その結果は破棄されます。しかし、someFunction() が他のパッケージレベルの変数や関数に依存する場合、その依存関係が初期化順序に正しく反映される必要があります。

Goコンパイラ (cmd/gc) と初期化処理

cmd/gc はGo言語の公式コンパイラです。コンパイラはソースコードを解析し、実行可能なバイナリを生成します。この過程で、パッケージレベルの変数の初期化順序を決定し、適切な初期化コードを生成する役割を担っています。

sinit.c は、Goコンパイラのバックエンドの一部であり、初期化順序の計算("static initialization")を担当するファイルです。このファイル内の init1 関数は、初期化が必要なノード(変数や関数)を走査し、それらの依存関係を解決して正しい初期化順序を構築します。

技術的詳細

このコミットの技術的な核心は、src/cmd/gc/sinit.c 内の init1 関数の変更にあります。init1 関数は、Goプログラムの初期化順序を決定するための主要なロジックを含んでいます。

変更前は、init1 関数がノードを走査する際に、関数内部のブランク識別子(isblank(n) && n->curfn == N の条件を満たさないもの)については、初期化順序の計算から除外していました。これは、関数スコープ内のブランク変数は、パッケージレベルの初期化順序には影響しないため、通常は正しい挙動です。

しかし、トップレベルのブランク変数(n->curfn == N、つまりカレント関数が Null、つまりグローバルスコープ)の場合でも、isblank(n) が真であれば、break; ではなく、n->defn->initorder = InitDone;*out = list(*out, n->defn); が実行されていました。これは、ブランク変数の定義を初期化済みとしてマークし、初期化リストに追加する処理です。しかし、この処理が不完全であったため、依存関係が正しく解決されませんでした。

また、staticinit 関数は、ノードが静的に初期化可能かどうかを判断し、その初期化を処理します。変更前は、ブランク識別子の場合でも staticinit が呼び出されていました。

このコミットは、以下の2つの主要な変更を導入します。

  1. 関数内部のブランク識別子の処理の明確化: if(isblank(n) && n->curfn == N && n->defn != N && n->defn->initorder == InitNotStarted) ブロック内で、break; が追加されました。これは、関数内部のブランク識別子については、初期化順序の計算をそこで中断し、それ以上処理を進めないことを意味します。これにより、関数スコープのブランク変数が誤ってパッケージレベルの初期化順序に影響を与えることを防ぎます。

  2. トップレベルのブランク変数の初期化順序への組み込み: if(!staticinit(n, out)) { の条件が if(isblank(n) || !staticinit(n, out)) { に変更されました。 この変更により、もしノード n がブランク識別子である場合(isblank(n) が真)、staticinit(n, out) の結果に関わらず、そのノードの定義(n->defn)が初期化リスト *out に追加されるようになります。 これは、トップレベルのブランク変数への代入が、その代入式に含まれる他の変数や関数の初期化に依存する場合、その依存関係を初期化グラフに明示的に組み込むことを意味します。これにより、var _ = f() + g() のようなケースで、fg が完全に初期化されるまで f() + g() の評価が待機されるようになります。

これらの変更により、Goコンパイラは、トップレベルのブランク変数への代入も初期化順序の依存関係として正しく考慮するようになり、Issue 5244で報告されたような初期化順序の誤りによるパニックが解消されます。

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

--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -53,9 +53,7 @@ init1(Node *n, NodeList **out)
 		if(isblank(n) && n->curfn == N && n->defn != N && n->defn->initorder == InitNotStarted) {
 			// blank names initialization is part of init() but not
 			// when they are inside a function.
-\t\t\tn->defn->initorder = InitDone;
-\t\t\tif(debug['%']) dump("nonstatic", n->defn);
-\t\t\t*out = list(*out, n->defn);
+\t\t\tbreak;
 		}
 		return;
 	}
@@ -130,7 +128,7 @@ init1(Node *n, NodeList **out)
 				init2(n->defn->right, out);
 				if(debug['j'])
 					print("%S\n", n->sym);
-\t\t\t\tif(!staticinit(n, out)) {
+\t\t\t\tif(isblank(n) || !staticinit(n, out)) {
 					if(debug['%']) dump("nonstatic", n->defn);
 					*out = list(*out, n->defn);
 				}
--- /dev/null
+++ b/test/fixedbugs/issue5244.go
@@ -0,0 +1,18 @@
+// run
+
+// Copyright 2013 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.
+
+// Issue 5244: the init order computation uses the wrong
+// order for top-level blank identifier assignments.
+// The example used to panic because it tries calling a
+// nil function instead of assigning to f before.
+
+package main
+
+var f = func() int { return 1 }
+var _ = f() + g()
+var g = func() int { return 2 }
+
+func main() {}

コアとなるコードの解説

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

  1. 最初の変更ブロック:

    -			n->defn->initorder = InitDone;
    -			if(debug['%']) dump("nonstatic", n->defn);
    -			*out = list(*out, n->defn);
    +			break;
    

    この変更は、init1 関数内の特定の条件ブロック(isblank(n) && n->curfn == N && n->defn != N && n->defn->initorder == InitNotStarted)に適用されます。 変更前は、この条件に合致するノード(トップレベルのブランク変数)に対して、InitDone とマークし、初期化リスト *out に追加していました。しかし、この処理だけでは依存関係が完全に解決されず、問題を引き起こしていました。 変更後は、単に break; とすることで、この特定のケースでの処理を中断します。これは、後述の変更でより包括的にトップレベルのブランク変数を処理するため、ここでは不要になったか、あるいは誤った処理を避けるためのものです。

  2. 二番目の変更ブロック:

    -			if(!staticinit(n, out)) {
    +			if(isblank(n) || !staticinit(n, out)) {
    

    これは init1 関数内の別の場所にある条件文の変更です。 変更前は、staticinit(n, out)false を返す(つまり、ノード n が静的に初期化できない)場合にのみ、そのノードの定義を初期化リスト *out に追加していました。 変更後は、条件に isblank(n) が追加されました。これにより、ノード n がブランク識別子である場合、staticinit(n, out) の結果に関わらず、そのノードの定義が初期化リスト *out に追加されるようになります。 この変更が、Issue 5244の修正の核心です。トップレベルのブランク変数への代入(例: var _ = f() + g())は、それ自体が静的に初期化可能であるかどうかに関わらず、その代入式が持つ依存関係(この場合は f()g() の呼び出し)を初期化順序に組み込む必要があります。この変更により、ブランク識別子も初期化順序のグラフに適切に組み込まれ、依存する関数や変数が初期化されるまで評価が待機されるようになります。

test/fixedbugs/issue5244.go の追加

このファイルは、Issue 5244で報告されたバグを再現するための新しいテストケースです。

  • // run コメントは、このファイルが実行可能なテストであることを示します。
  • var f = func() int { return 1 }var g = func() int { return 2 } は、パッケージレベルで関数リテラルを変数に代入しています。
  • var _ = f() + g() が問題の核心です。この行は、f()g() の結果を合計し、その結果をブランク識別子に代入しています。 テストファイルのコメントにあるように、このコードは以前のコンパイラでは gnil の状態で呼び出され、パニックを引き起こしていました。このコミットの修正により、fg が正しく初期化された後に f() + g() が評価されるようになり、テストが成功するようになります。

関連リンク

  • Gerrit Change-Id: https://golang.org/cl/8601044 (GoプロジェクトのコードレビューシステムであるGerritの変更リストへのリンク)

参考にした情報源リンク

  • Go言語の公式ドキュメント (初期化順序に関する記述)
  • Goコンパイラのソースコード (src/cmd/gc/sinit.c および関連ファイル)
  • Go言語のIssueトラッカー (Issue 5244に関する議論があれば)
    • 今回の検索では直接的なIssue 5244の情報は見つかりませんでしたが、通常はGoのGitHubリポジトリのIssuesセクションで詳細が確認できます。