[インデックス 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言語の一般的な知識