[インデックス 13276] ファイルの概要
このコミットは、Go言語の標準ライブラリのベンチマークテストスイートの一部である test/bench/go1
ディレクトリ内のテストファイルに対する修正です。具体的には、gob_test.go
と json_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.go
の init()
よりも先に実行されてしまう可能性があり、その結果、jsondata
が未初期化の状態でアクセスされ、ベンチマークテストが失敗する、あるいは予期せぬ動作を引き起こす可能性がありました。
このコミットは、このような脆弱な依存関係を解消し、ベンチマークテストの初期化プロセスをより堅牢で予測可能なものにすることを目的としています。
前提知識の解説
Go言語の init()
関数
Go言語の init()
関数は、各パッケージに複数定義できる特別な関数です。これらの関数は、パッケージがインポートされた際、またはプログラムの実行開始時(main
パッケージの場合)に、main()
関数が呼び出されるよりも前に自動的に実行されます。init()
関数は、パッケージレベルの変数の初期化、プログラムの状態のセットアップ、外部リソースへの接続など、プログラムの実行開始前に一度だけ実行する必要がある処理に利用されます。
重要な点: Go言語の仕様では、異なるソースファイルに定義された init()
関数の実行順序は保証されていません。同じファイル内の複数の init()
関数は定義された順に実行されますが、異なるファイル間ではコンパイラやリンカの実装に依存し、予測不可能な順序で実行される可能性があります。この特性が、本コミットで修正される問題の根源となっています。
Go言語におけるベンチマークテスト
Go言語には、標準でベンチマークテストをサポートする機能が組み込まれています。testing
パッケージを使用し、BenchmarkXxx
という命名規則に従って関数を定義することで、コードのパフォーマンスを測定できます。ベンチマークテストは go test -bench=.
コマンドで実行され、指定された処理を複数回実行し、その平均実行時間やメモリ割り当てなどを測定します。
ベンチマークテストでは、テスト対象のコードが実行される前に、必要なデータや環境を適切に初期化することが重要です。この初期化が不適切だと、ベンチマーク結果の信頼性が損なわれたり、テスト自体が失敗したりする可能性があります。
json
と gob
のデータエンコーディング/デコーディング
- JSON (JavaScript Object Notation): 軽量なデータ交換フォーマットで、人間が読み書きしやすく、機械が解析・生成しやすいという特徴があります。Go言語の
encoding/json
パッケージは、Goの構造体とJSONデータの間の変換(マーシャリング/アンマーシャリング)を提供します。 - Gob: Go言語独自のバイナリエンコーディングフォーマットです。Goプログラム間でデータを効率的にシリアライズ・デシリアライズするために設計されており、JSONよりもコンパクトで高速な場合があります。Go言語の
encoding/gob
パッケージがこれを提供します。
これらのフォーマットは、ベンチマークテストにおいて、実際のアプリケーションで処理される可能性のあるデータを表現するために使用されます。
技術的詳細
このコミットの技術的な核心は、init()
関数の実行順序の非保証性というGo言語の特性を回避し、初期化の依存関係をより堅牢な方法で管理することにあります。
元の実装では、json_test.go
内の init()
関数がグローバル変数 jsonbytes
と jsondata
を初期化し、さらに gob_test.go
内の gobinit()
関数(これも実質的には初期化関数)を呼び出していました。gobinit()
は jsondata
に依存しているため、json_test.go
の init()
が先に完了している必要がありました。
このコミットでは、この問題を解決するために以下の戦略が採用されました。
-
初期化ロジックの関数化:
jsonbytes
とjsondata
の生成ロジックを、それぞれ独立した関数makeJsonBytes()
とmakeJsonData()
に切り出しました。 -
変数宣言時の関数呼び出し: グローバル変数
jsonbytes
とjsondata
の宣言時に、これらの新しい関数を呼び出して値を割り当てるように変更しました。// 変更前: // var ( // jsonbytes []byte // jsondata JSONResponse // ) // func init() { // // ... jsonbytes の生成 ... // // ... jsondata の生成 ... // gobinit() // ここでgobinitを呼び出していた // } // 変更後: var ( jsonbytes = makeJsonBytes() jsondata = makeJsonData() ) // makeJsonBytes() と makeJsonData() は独立した関数として定義
この変更により、jsonbytes
と jsondata
の初期化は、init()
関数内で行われるのではなく、変数が宣言されるタイミングでコンパイラによって解決されるようになりました。Goコンパイラは、変数の依存関係を解析し、jsonbytes
が makeJsonData()
の中で使用されることを認識するため、makeJsonBytes()
が makeJsonData()
よりも先に実行されることを保証できます。これにより、init()
関数の実行順序に依存することなく、必要なデータが常に正しい順序で初期化されるようになります。
また、gob_test.go
の gobinit()
関数は、json_test.go
の init()
から明示的に呼び出される必要がなくなったため、自身の初期化ロジックとして標準の 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()
関数にリネームされました。json
のinit
の後に呼び出されるというコメントが削除されました。これは、もはやjson_test.go
のinit()
に依存しないためです。
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 {
jsonbytes
とjsondata
の宣言が変更され、それぞれmakeJsonBytes()
とmakeJsonData()
関数の戻り値で直接初期化されるようになりました。- 元の
init()
関数がmakeJsonBytes()
関数にリネームされ、jsonbytes
の生成ロジックのみを含むようになりました。 jsondata
の生成ロジックが新しいmakeJsonData()
関数に切り出されました。この関数はjsonbytes
を利用してJSONResponse
オブジェクトを生成し、返します。makeJsonData()
関数内からgobinit()
の呼び出しが削除されました。
コアとなるコードの解説
このコミットの核心は、Go言語の init()
関数の実行順序が保証されないという特性を回避し、初期化の依存関係をコンパイラに委ねることで、より堅牢なコードを実現した点にあります。
-
json_test.go
の変更:- 以前は、
jsonbytes
とjsondata
はグローバル変数として宣言され、その初期化はinit()
関数内で行われていました。このinit()
関数は、gob_test.go
のgobinit()
を呼び出す責任も持っていました。 - 変更後、
jsonbytes
とjsondata
は、それぞれmakeJsonBytes()
とmakeJsonData()
という関数を呼び出すことで直接初期化されるようになりました。 var jsonbytes = makeJsonBytes()
のように、グローバル変数の宣言時に関数呼び出しの結果で初期化する場合、Goコンパイラはこれらの初期化式が評価される順序を、依存関係に基づいて適切に決定します。この場合、makeJsonData()
がjsonbytes
に依存しているため、コンパイラはmakeJsonBytes()
がmakeJsonData()
よりも先に実行されることを保証します。- これにより、
init()
関数の非決定的な実行順序に依存することなく、jsonbytes
がjsondata
の初期化前に確実に利用可能になります。 - また、
gobinit()
の呼び出しがjson_test.go
から削除されたことで、json_test.go
はgob_test.go
の初期化に責任を持つ必要がなくなり、モジュール間の独立性が高まりました。
- 以前は、
-
gob_test.go
の変更:gobinit()
関数がinit()
にリネームされました。これは、json_test.go
からの明示的な呼び出しがなくなったため、gob_test.go
自身がパッケージ初期化時に実行すべきロジックとして、標準のinit()
関数を利用できるようになったことを意味します。gob_test.go
のinit()
関数は、jsondata
が既に初期化されていることを前提としてgobResponse(&jsondata)
を呼び出します。この依存関係は、json_test.go
でのjsondata
の初期化がinit()
順序に依存しない堅牢な方法に変更されたことで、安全に満たされるようになりました。
この修正により、ベンチマークテストの初期化プロセスがより予測可能で信頼性の高いものとなり、将来的に新たなベンチマークが追加された際にも、init()
関数の実行順序に関する潜在的な問題を回避できるようになりました。
関連リンク
特になし。
参考にした情報源リンク
- Go言語の
init()
関数に関する公式ドキュメントやブログ記事(Go言語の仕様におけるinit()
関数の実行順序の非保証性について)。 - Go言語のベンチマークテストに関する公式ドキュメント。
- Go言語の
encoding/json
およびencoding/gob
パッケージのドキュメント。