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

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

このコミットは、Go言語の標準ライブラリのベンチマークテストスイートの一部である test/bench/go1 ディレクトリ内のテストファイルに対する修正です。具体的には、gob_test.gojson_test.go という2つのファイルが対象となっており、Go 1リリース時のベンチマークテストの安定性と信頼性を向上させることを目的としています。特に、gzip テストの修正と、init() 関数の実行順序に依存しない堅牢な初期化メカニズムの導入が焦点となっています。

コミット

commit 6b4ae1d28e7a33c84e049c65c4fe658a6956d11d
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Tue Jun 5 00:14:39 2012 +0800

    test/bench/go1: fix gzip test
          We can't depend on init() order, and certainly we don't want to
    register all future benchmarks that use jsonbytes or jsondata to init()
    in json_test.go, so we use a more general solution: make generation of
    jsonbytes and jsondata their own function so that the compiler will take
    care of the order.
    
    R=golang-dev, dave, rsc
    CC=golang-dev
    https://golang.org/cl/6282046

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

https://github.com/golang/go/commit/6b4ae1d28e7a33c84e049c65c4fe658a6956d11d

元コミット内容

test/bench/go1: fix gzip test
      We can't depend on init() order, and certainly we don't want to
register all future benchmarks that use jsonbytes or jsondata to init()
in json_test.go, so we use a more general solution: make generation of
jsonbytes and jsondata their own function so that the compiler will take
care of the order.

R=golang-dev, dave, rsc
CC=golang-dev
https://golang.org/cl/6282046

変更の背景

このコミットの主な背景は、Go言語の init() 関数の実行順序に関する問題です。Go言語では、パッケージ内の init() 関数は main() 関数が実行される前に自動的に呼び出されますが、異なるファイルやパッケージに存在する複数の init() 関数の実行順序は保証されていません

元のコードでは、gob_test.go 内の gobinit() 関数(後に init() に変更される)が、json_test.go 内で init() 関数によって初期化される jsondata 変数に依存していました。つまり、gobinit() が実行される前に jsondata が完全に準備されている必要がありました。しかし、init() 関数の実行順序が保証されないため、gobinit()json_test.goinit() よりも先に実行されてしまう可能性があり、その結果、jsondata が未初期化の状態でアクセスされ、ベンチマークテストが失敗する、あるいは予期せぬ動作を引き起こす可能性がありました。

このコミットは、このような脆弱な依存関係を解消し、ベンチマークテストの初期化プロセスをより堅牢で予測可能なものにすることを目的としています。

前提知識の解説

Go言語の init() 関数

Go言語の init() 関数は、各パッケージに複数定義できる特別な関数です。これらの関数は、パッケージがインポートされた際、またはプログラムの実行開始時(main パッケージの場合)に、main() 関数が呼び出されるよりも前に自動的に実行されます。init() 関数は、パッケージレベルの変数の初期化、プログラムの状態のセットアップ、外部リソースへの接続など、プログラムの実行開始前に一度だけ実行する必要がある処理に利用されます。

重要な点: Go言語の仕様では、異なるソースファイルに定義された init() 関数の実行順序は保証されていません。同じファイル内の複数の init() 関数は定義された順に実行されますが、異なるファイル間ではコンパイラやリンカの実装に依存し、予測不可能な順序で実行される可能性があります。この特性が、本コミットで修正される問題の根源となっています。

Go言語におけるベンチマークテスト

Go言語には、標準でベンチマークテストをサポートする機能が組み込まれています。testing パッケージを使用し、BenchmarkXxx という命名規則に従って関数を定義することで、コードのパフォーマンスを測定できます。ベンチマークテストは go test -bench=. コマンドで実行され、指定された処理を複数回実行し、その平均実行時間やメモリ割り当てなどを測定します。

ベンチマークテストでは、テスト対象のコードが実行される前に、必要なデータや環境を適切に初期化することが重要です。この初期化が不適切だと、ベンチマーク結果の信頼性が損なわれたり、テスト自体が失敗したりする可能性があります。

jsongob のデータエンコーディング/デコーディング

  • JSON (JavaScript Object Notation): 軽量なデータ交換フォーマットで、人間が読み書きしやすく、機械が解析・生成しやすいという特徴があります。Go言語の encoding/json パッケージは、Goの構造体とJSONデータの間の変換(マーシャリング/アンマーシャリング)を提供します。
  • Gob: Go言語独自のバイナリエンコーディングフォーマットです。Goプログラム間でデータを効率的にシリアライズ・デシリアライズするために設計されており、JSONよりもコンパクトで高速な場合があります。Go言語の encoding/gob パッケージがこれを提供します。

これらのフォーマットは、ベンチマークテストにおいて、実際のアプリケーションで処理される可能性のあるデータを表現するために使用されます。

技術的詳細

このコミットの技術的な核心は、init() 関数の実行順序の非保証性というGo言語の特性を回避し、初期化の依存関係をより堅牢な方法で管理することにあります。

元の実装では、json_test.go 内の init() 関数がグローバル変数 jsonbytesjsondata を初期化し、さらに gob_test.go 内の gobinit() 関数(これも実質的には初期化関数)を呼び出していました。gobinit()jsondata に依存しているため、json_test.goinit() が先に完了している必要がありました。

このコミットでは、この問題を解決するために以下の戦略が採用されました。

  1. 初期化ロジックの関数化: jsonbytesjsondata の生成ロジックを、それぞれ独立した関数 makeJsonBytes()makeJsonData() に切り出しました。

  2. 変数宣言時の関数呼び出し: グローバル変数 jsonbytesjsondata の宣言時に、これらの新しい関数を呼び出して値を割り当てるように変更しました。

    // 変更前:
    // var (
    // 	jsonbytes []byte
    // 	jsondata  JSONResponse
    // )
    // func init() {
    //     // ... jsonbytes の生成 ...
    //     // ... jsondata の生成 ...
    //     gobinit() // ここでgobinitを呼び出していた
    // }
    
    // 変更後:
    var (
    	jsonbytes = makeJsonBytes()
    	jsondata  = makeJsonData()
    )
    // makeJsonBytes() と makeJsonData() は独立した関数として定義
    

この変更により、jsonbytesjsondata の初期化は、init() 関数内で行われるのではなく、変数が宣言されるタイミングでコンパイラによって解決されるようになりました。Goコンパイラは、変数の依存関係を解析し、jsonbytesmakeJsonData() の中で使用されることを認識するため、makeJsonBytes()makeJsonData() よりも先に実行されることを保証できます。これにより、init() 関数の実行順序に依存することなく、必要なデータが常に正しい順序で初期化されるようになります。

また、gob_test.gogobinit() 関数は、json_test.goinit() から明示的に呼び出される必要がなくなったため、自身の初期化ロジックとして標準の init() 関数に名前を変更しました。これにより、gob_test.go は自身の初期化を独立して管理できるようになり、モジュール間の結合度が低下し、コードの可読性と保守性が向上しました。

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

test/bench/go1/gob_test.go

--- a/test/bench/go1/gob_test.go
+++ b/test/bench/go1/gob_test.go
@@ -21,9 +21,7 @@ var (
 	gobdata  *JSONResponse
 )
 
-func gobinit() {
-	// gobinit is called after json's init,
-	// because it uses jsondata.
+func init() {
 	gobdata = gobResponse(&jsondata)
 
 	var buf bytes.Buffer
  • gobinit() 関数が init() 関数にリネームされました。
  • jsoninit の後に呼び出されるというコメントが削除されました。これは、もはや json_test.goinit() に依存しないためです。

test/bench/go1/json_test.go

--- a/test/bench/go1/json_test.go
+++ b/test/bench/go1/json_test.go
@@ -17,11 +17,11 @@ import (
 )
 
 var (
-	jsonbytes []byte
-	jsondata  JSONResponse
+	jsonbytes = makeJsonBytes()
+	jsondata  = makeJsonData()
 )
 
-func init() {
+func makeJsonBytes() []byte {
 	var r io.Reader
 	r = strings.NewReader(jsonbz2_base64)
 	r = base64.NewDecoder(base64.StdEncoding, r)
@@ -30,12 +30,15 @@ func init() {
 	if err != nil {
 		panic(err)
 	}
-	jsonbytes = b
+	return b
+}
 
-	if err := json.Unmarshal(jsonbytes, &jsondata); err != nil {
+func makeJsonData() JSONResponse {
+	var v JSONResponse
+	if err := json.Unmarshal(jsonbytes, &v); err != nil {
 		panic(err)
 	}
-\tgobinit()\n+\treturn v
 }
 
 type JSONResponse struct {
  • jsonbytesjsondata の宣言が変更され、それぞれ makeJsonBytes()makeJsonData() 関数の戻り値で直接初期化されるようになりました。
  • 元の init() 関数が makeJsonBytes() 関数にリネームされ、jsonbytes の生成ロジックのみを含むようになりました。
  • jsondata の生成ロジックが新しい makeJsonData() 関数に切り出されました。この関数は jsonbytes を利用して JSONResponse オブジェクトを生成し、返します。
  • makeJsonData() 関数内から gobinit() の呼び出しが削除されました。

コアとなるコードの解説

このコミットの核心は、Go言語の init() 関数の実行順序が保証されないという特性を回避し、初期化の依存関係をコンパイラに委ねることで、より堅牢なコードを実現した点にあります。

  1. json_test.go の変更:

    • 以前は、jsonbytesjsondata はグローバル変数として宣言され、その初期化は init() 関数内で行われていました。この init() 関数は、gob_test.gogobinit() を呼び出す責任も持っていました。
    • 変更後、jsonbytesjsondata は、それぞれ makeJsonBytes()makeJsonData() という関数を呼び出すことで直接初期化されるようになりました。
    • var jsonbytes = makeJsonBytes() のように、グローバル変数の宣言時に関数呼び出しの結果で初期化する場合、Goコンパイラはこれらの初期化式が評価される順序を、依存関係に基づいて適切に決定します。この場合、makeJsonData()jsonbytes に依存しているため、コンパイラは makeJsonBytes()makeJsonData() よりも先に実行されることを保証します。
    • これにより、init() 関数の非決定的な実行順序に依存することなく、jsonbytesjsondata の初期化前に確実に利用可能になります。
    • また、gobinit() の呼び出しが json_test.go から削除されたことで、json_test.gogob_test.go の初期化に責任を持つ必要がなくなり、モジュール間の独立性が高まりました。
  2. gob_test.go の変更:

    • gobinit() 関数が init() にリネームされました。これは、json_test.go からの明示的な呼び出しがなくなったため、gob_test.go 自身がパッケージ初期化時に実行すべきロジックとして、標準の init() 関数を利用できるようになったことを意味します。
    • gob_test.goinit() 関数は、jsondata が既に初期化されていることを前提として gobResponse(&jsondata) を呼び出します。この依存関係は、json_test.go での jsondata の初期化が init() 順序に依存しない堅牢な方法に変更されたことで、安全に満たされるようになりました。

この修正により、ベンチマークテストの初期化プロセスがより予測可能で信頼性の高いものとなり、将来的に新たなベンチマークが追加された際にも、init() 関数の実行順序に関する潜在的な問題を回避できるようになりました。

関連リンク

特になし。

参考にした情報源リンク

  • Go言語の init() 関数に関する公式ドキュメントやブログ記事(Go言語の仕様における init() 関数の実行順序の非保証性について)。
  • Go言語のベンチマークテストに関する公式ドキュメント。
  • Go言語の encoding/json および encoding/gob パッケージのドキュメント。