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

[インデックス 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.goinit 関数、universe.godef 関数、および Universe の使用と、universe.goinit 関数、および Universe の作成との間に初期化競合が存在していました。

パッケージ内の init 関数の実行順序は未指定です。golang.org 環境では、go ツールがテストを非テストソースの前にコンパイルするため、現在のところテストは壊れていません。しかし、他の環境では、例えばコンパイル前にソースファイルをソートする可能性があり、その結果この競合がトリガーされ、nil ポインタパニックを引き起こす可能性があります。

変更の背景

Go言語では、各パッケージに複数の init 関数を定義できます。これらの init 関数は、パッケージがインポートされた際に自動的に実行され、パッケージの初期化処理を行います。しかし、同じパッケージ内の複数の init 関数の実行順序はGo言語の仕様で保証されていません。また、異なるパッケージ間の init 関数の実行順序も、インポートグラフによって決定されますが、特定のケースでは予測が難しい場合があります。

このコミットの背景には、exp/types パッケージのテストコード check_test.go と、型システムの中核をなす universe.go の間で発生していた初期化のタイミングの問題があります。

check_test.goinit 関数では、テストのために asserttrace といった組み込み関数を宣言していました。これらの組み込み関数は、def 関数を通じて Universe スコープに登録される必要がありました。一方、universe.go では、Universe 変数自体が init 関数内で構築されていました。

問題は、check_test.goinit 関数が universe.goinit 関数よりも先に実行されてしまう可能性があったことです。もし check_test.goinit が先に実行されると、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 関数の実行順序が未指定であるという特性に起因する典型的な初期化競合です。

  1. 競合の発生源:

    • check_test.goinit 関数: テストに必要な asserttrace といった組み込み関数を def 関数を使って Universe スコープに登録しようとしていました。
    • universe.goinit 関数: Universe 変数自体を初期化し、構築していました。
  2. 競合のメカニズム:

    • Goの仕様では、同じパッケージ内の複数の init 関数の実行順序は保証されません。
    • もし、check_test.goinit 関数が universe.goinit 関数よりも先に実行されてしまった場合、check_test.goinit 内で def 関数が Universe にアクセスしようとした時点で、Universe がまだ nil であるか、完全に初期化されていない状態である可能性がありました。
    • nilUniverse に対して操作を行おうとすると、nil ポインタパニックが発生し、テストがクラッシュします。
  3. 環境依存性:

    • golang.org のビルド環境では、go ツールが _test.go ファイルを非テストファイルよりも後にコンパイルする傾向があったため、結果的に universe.goinit が先に実行され、Universe が適切に初期化されていました。このため、競合が表面化していませんでした。
    • しかし、これは go ツールの実装の詳細に依存する挙動であり、他の環境(例えば、ソースファイルをアルファベット順にソートしてからコンパイルするような環境)では、check_test.go が先にコンパイルされ、その init 関数が先に実行されることで、問題が顕在化する可能性がありました。
  4. 解決策:

    • check_test.go から init 関数を削除し、組み込み関数の宣言処理を TestCheck 関数内に移動しました。
    • TestCheck 関数は、テストが実行される際に呼び出される通常の関数であり、init 関数とは異なり、その実行タイミングはより予測可能です。
    • TestCheck が実行される時点では、universe.goinit 関数は既に実行され、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 内でテスト用の組み込み関数 (asserttrace) を宣言するタイミングの変更です。

  1. 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.goinit 関数(Universe 変数を構築する)との間で保証されていませんでした。これが初期化競合の原因でした。この init 関数ブロックが完全に削除されました。
  2. 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 関数の特性を深く理解し、その未指定の実行順序が引き起こす潜在的な問題を回避するための、堅実な設計判断を示しています。

関連リンク

参考にした情報源リンク

  • https://golang.org/cl/6827076 (元のGo Gerritの変更リスト)
  • Go言語の公式ドキュメントおよびブログ
  • Go言語のソースコードリポジトリ
  • Go言語の init 関数とパッケージ初期化に関する一般的な知識
  • nil ポインタパニックに関するGo言語の一般的な知識