[インデックス 14382] ファイルの概要
このコミットは、Go言語の実験的な型チェッカーパッケージ exp/types
内のテストファイル check_test.go
における初期化競合(init race)を解消することを目的としています。具体的には、テストで使用される組み込み関数の宣言が、Universe
変数の構築と競合する可能性があった問題を修正しています。
コミット
commit 80f4ff226ff8fb36ba3c8b4808982c6664ce47c3
Author: David Symonds <dsymonds@golang.org>
Date: Tue Nov 13 09:08:33 2012 +1100
exp/types: avoid init race in check_test.go.
There was an init race between
check_test.go:init
universe.go:def
use of Universe
and
universe.go:init
creation of Universe
The order in which init funcs are executed in a package is unspecified.
The test is not currently broken in the golang.org environment
because the go tool compiles the test with non-test sources before test sources,
but other environments may, say, sort the source files before compiling,
and thus trigger this race, causing a nil pointer panic.
R=gri
CC=golang-dev
https://golang.org/cl/6827076
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/80f4ff226ff8fb36ba3c8b4808982c6664ce47c3
元コミット内容
exp/types: avoid init race in check_test.go.
このコミットは、check_test.go
内での初期化競合を回避します。
check_test.go
の init
関数、universe.go
の def
関数、および Universe
の使用と、universe.go
の init
関数、および Universe
の作成との間に初期化競合が存在していました。
パッケージ内の init
関数の実行順序は未指定です。golang.org
環境では、go
ツールがテストを非テストソースの前にコンパイルするため、現在のところテストは壊れていません。しかし、他の環境では、例えばコンパイル前にソースファイルをソートする可能性があり、その結果この競合がトリガーされ、nil
ポインタパニックを引き起こす可能性があります。
変更の背景
Go言語では、各パッケージに複数の init
関数を定義できます。これらの init
関数は、パッケージがインポートされた際に自動的に実行され、パッケージの初期化処理を行います。しかし、同じパッケージ内の複数の init
関数の実行順序はGo言語の仕様で保証されていません。また、異なるパッケージ間の init
関数の実行順序も、インポートグラフによって決定されますが、特定のケースでは予測が難しい場合があります。
このコミットの背景には、exp/types
パッケージのテストコード check_test.go
と、型システムの中核をなす universe.go
の間で発生していた初期化のタイミングの問題があります。
check_test.go
の init
関数では、テストのために assert
や trace
といった組み込み関数を宣言していました。これらの組み込み関数は、def
関数を通じて Universe
スコープに登録される必要がありました。一方、universe.go
では、Universe
変数自体が init
関数内で構築されていました。
問題は、check_test.go
の init
関数が universe.go
の init
関数よりも先に実行されてしまう可能性があったことです。もし check_test.go
の init
が先に実行されると、def
関数が Universe
変数にアクセスしようとした際に、Universe
がまだ完全に初期化されていない(例えば nil
の状態である)ため、nil
ポインタパニックが発生する可能性がありました。
golang.org
のビルド環境では、go
ツールがテスト関連のソースファイル (_test.go
で終わるファイル) を非テストソースファイルよりも後にコンパイルする傾向があったため、この競合が表面化していませんでした。しかし、これはツールの実装に依存する挙動であり、他のビルド環境や異なるバージョンの go
ツールでは、ソースファイルのコンパイル順序が変わり、この競合が顕在化して nil
ポインタパニックを引き起こす危険性がありました。このコミットは、このような環境依存の脆弱性を排除し、テストの堅牢性を高めるために行われました。
前提知識の解説
Go言語の init
関数
Go言語の init
関数は、パッケージの初期化のために使用される特別な関数です。
- 自動実行:
init
関数は、パッケージがインポートされた際に、main
関数が実行される前に自動的に実行されます。 - 引数なし、戻り値なし:
init
関数は引数を取らず、戻り値もありません。 - 複数定義可能: 1つのパッケージ内に複数の
init
関数を定義できます。 - 実行順序:
- パッケージ内: 同じパッケージ内の複数の
init
関数の実行順序は保証されません。コンパイラがソースファイルを処理する順序に依存する場合があります。 - パッケージ間: パッケージ間の
init
関数の実行順序は、インポートグラフの依存関係によって決定されます。依存されるパッケージのinit
関数が、依存するパッケージのinit
関数よりも先に実行されます。
- パッケージ内: 同じパッケージ内の複数の
- 用途: グローバル変数の初期化、設定ファイルの読み込み、データベース接続の確立など、プログラムの実行開始前に一度だけ行われるべき処理に利用されます。
nil
ポインタパニック
Go言語において、nil
ポインタパニックは、nil
値を持つポインタを通じてメソッドを呼び出したり、フィールドにアクセスしようとした際に発生するランタイムエラーです。これは、参照しようとしているメモリ領域が割り当てられていない、または有効なオブジェクトを指していないことを意味します。
exp/types
パッケージ (Go言語の型チェッカー)
exp/types
は、Go言語のコンパイラやツールチェーンの一部として、Goプログラムの型チェックを行うための実験的なパッケージでした。Go言語の型システムは非常に複雑であり、このパッケージはその型チェックロジックを実装していました。
Universe
スコープ: Go言語の型システムにおいて、Universe
スコープは、int
,string
,bool
などの組み込み型や、len
,cap
,make
,new
などの組み込み関数が定義されている最も外側のスコープを指します。型チェッカーは、プログラム内の識別子を解決する際に、まずこのUniverse
スコープを参照します。- 組み込み関数の宣言:
exp/types
パッケージでは、型チェックの過程でこれらの組み込み型や組み込み関数を認識し、適切に処理する必要があります。そのため、テスト環境においても、これらの組み込み要素がUniverse
スコープに正しく登録されていることが前提となります。
技術的詳細
このコミットで修正された問題は、Go言語の init
関数の実行順序が未指定であるという特性に起因する典型的な初期化競合です。
-
競合の発生源:
check_test.go
のinit
関数: テストに必要なassert
やtrace
といった組み込み関数をdef
関数を使ってUniverse
スコープに登録しようとしていました。universe.go
のinit
関数:Universe
変数自体を初期化し、構築していました。
-
競合のメカニズム:
- Goの仕様では、同じパッケージ内の複数の
init
関数の実行順序は保証されません。 - もし、
check_test.go
のinit
関数がuniverse.go
のinit
関数よりも先に実行されてしまった場合、check_test.go
のinit
内でdef
関数がUniverse
にアクセスしようとした時点で、Universe
がまだnil
であるか、完全に初期化されていない状態である可能性がありました。 nil
のUniverse
に対して操作を行おうとすると、nil
ポインタパニックが発生し、テストがクラッシュします。
- Goの仕様では、同じパッケージ内の複数の
-
環境依存性:
golang.org
のビルド環境では、go
ツールが_test.go
ファイルを非テストファイルよりも後にコンパイルする傾向があったため、結果的にuniverse.go
のinit
が先に実行され、Universe
が適切に初期化されていました。このため、競合が表面化していませんでした。- しかし、これは
go
ツールの実装の詳細に依存する挙動であり、他の環境(例えば、ソースファイルをアルファベット順にソートしてからコンパイルするような環境)では、check_test.go
が先にコンパイルされ、そのinit
関数が先に実行されることで、問題が顕在化する可能性がありました。
-
解決策:
check_test.go
からinit
関数を削除し、組み込み関数の宣言処理をTestCheck
関数内に移動しました。TestCheck
関数は、テストが実行される際に呼び出される通常の関数であり、init
関数とは異なり、その実行タイミングはより予測可能です。TestCheck
が実行される時点では、universe.go
のinit
関数は既に実行され、Universe
変数は確実に初期化されているため、nil
ポインタパニックのリスクがなくなります。
この変更により、テストの初期化処理が init
関数の未指定の実行順序に依存しなくなり、より堅牢で移植性の高いコードになりました。
コアとなるコードの変更箇所
変更は src/pkg/exp/types/check_test.go
ファイルに集中しています。
--- a/src/pkg/exp/types/check_test.go
+++ b/src/pkg/exp/types/check_test.go
@@ -37,12 +37,6 @@ import (
var listErrors = flag.Bool("list", false, "list errors")
-func init() {
- // declare builtins for testing
- def(ast.Fun, "assert").Type = &builtin{aType, _Assert, "assert", 1, false, true}
- def(ast.Fun, "trace").Type = &builtin{aType, _Trace, "trace", 0, true, true}
-}
-
// The test filenames do not end in .go so that they are invisible
// to gofmt since they contain comments that must not change their
// positions relative to surrounding tokens.
@@ -241,6 +235,12 @@ func checkFiles(t *testing.T, testname string, testfiles []string) {
}
func TestCheck(t *testing.T) {
+ // Declare builtins for testing.
+ // Not done in an init func to avoid an init race with
+ // the construction of the Universe var.
+ def(ast.Fun, "assert").Type = &builtin{aType, _Assert, "assert", 1, false, true}
+ def(ast.Fun, "trace").Type = &builtin{aType, _Trace, "trace", 0, true, true}
+
// For easy debugging w/o changing the testing code,
// if there is a local test file, only test that file.
const testfile = "testdata/test.go"
コアとなるコードの解説
このコミットの核心は、check_test.go
内でテスト用の組み込み関数 (assert
と trace
) を宣言するタイミングの変更です。
-
init
関数の削除:- 元のコードでは、
init
関数内で以下の行が実行されていました。func init() { // declare builtins for testing def(ast.Fun, "assert").Type = &builtin{aType, _Assert, "assert", 1, false, true} def(ast.Fun, "trace").Type = &builtin{aType, _Trace, "trace", 0, true, true} }
- この
init
関数は、パッケージがロードされる際に自動的に実行されますが、その実行順序はuniverse.go
のinit
関数(Universe
変数を構築する)との間で保証されていませんでした。これが初期化競合の原因でした。このinit
関数ブロックが完全に削除されました。
- 元のコードでは、
-
TestCheck
関数への移動:- 削除された
init
関数内のコードは、TestCheck
関数内に移動されました。func TestCheck(t *testing.T) { // Declare builtins for testing. // Not done in an init func to avoid an init race with // the construction of the Universe var. def(ast.Fun, "assert").Type = &builtin{aType, _Assert, "assert", 1, false, true} def(ast.Fun, "trace").Type = &builtin{aType, _Trace, "trace", 0, true, true} // ... (rest of TestCheck function) }
TestCheck
はGoのテストフレームワーク (testing
パッケージ) によって呼び出される通常のテスト関数です。テスト関数が実行される時点では、関連するすべてのパッケージのinit
関数は既に実行され、グローバル変数(この場合はUniverse
)は完全に初期化されていることが保証されます。- これにより、
def
関数がUniverse
にアクセスする際に、Universe
が常に有効な状態であることが保証され、nil
ポインタパニックのリスクが完全に排除されました。
- 削除された
この変更は、Go言語の init
関数の特性を深く理解し、その未指定の実行順序が引き起こす潜在的な問題を回避するための、堅実な設計判断を示しています。
関連リンク
- Go言語の
init
関数に関する公式ドキュメントやブログ記事 - Go言語の
exp/types
パッケージに関する情報 (現在はgo/types
に統合されています)
参考にした情報源リンク
- https://golang.org/cl/6827076 (元のGo Gerritの変更リスト)
- Go言語の公式ドキュメントおよびブログ
- Go言語のソースコードリポジトリ
- Go言語の
init
関数とパッケージ初期化に関する一般的な知識 nil
ポインタパニックに関するGo言語の一般的な知識