[インデックス 13350] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/gob パッケージにおけるエンコーディングエンジンのキャッシュに関するバグ修正と、それに対応するテストの追加を行っています。具体的には、以下の2つのファイルが変更されました。
src/pkg/encoding/gob/encode.go:gobエンコーディングエンジンの取得・コンパイルロジックが含まれる主要なファイルです。このファイルで、壊れたエンコーディングエンジンがキャッシュされるのを防ぐための修正が加えられました。src/pkg/encoding/gob/encoder_test.go:gobエンコーダのテストファイルです。このコミットでは、修正されたバグ(Issue 3273)を再現し、修正が正しく機能することを確認するための新しいテストケースが追加されました。
コミット
commit 733ee91786ef4fd8a13a272745f0458a3ed74e50
Author: Rob Pike <r@golang.org>
Date: Wed Jun 13 15:55:43 2012 -0700
encoding/gob: don't cache broken encoding engines.
Fixes a situation where a nested bad type would still
permit the outer type to install a working engine, leading
to inconsistent behavior.
Fixes #3273.
R=bsiegert, rsc
CC=golang-dev
https://golang.org/cl/6294067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/733ee91786ef4fd8a13a272745f0458a3ed74e50
元コミット内容
encoding/gob: don't cache broken encoding engines.
Fixes a situation where a nested bad type would still
permit the outer type to install a working engine, leading
to inconsistent behavior.
Fixes #3273.
変更の背景
このコミットは、Go言語の encoding/gob パッケージにおける、特定の状況下でのエンコーディングの一貫性のない挙動を修正するために行われました。具体的には、Issue 3273で報告された問題に対処しています。
問題の核心は、gob がGoのデータ構造をエンコードする際に、その型に対応する「エンコーディングエンジン」をコンパイルし、これをキャッシュする仕組みにありました。通常、型が gob でエンコードできない場合(例えば、エクスポートされていないフィールドを持つ構造体など)、エンコーディングはエラーとなるべきです。しかし、このバグが存在する状況では、以下のような問題が発生していました。
- ネストされた不正な型: ある構造体
Aが、gobでエンコードできない別の構造体Bをフィールドとして持っている場合。 - 部分的なエンジン構築とキャッシュ:
gobがAのエンコーディングエンジンを構築しようとすると、その過程でBのエンコーディングエンジンも構築しようとします。もしBの構築が失敗した場合でも、Aのエンジンが部分的に構築された状態でキャッシュされてしまう可能性がありました。 - 一貫性のない挙動: その結果、
Aのエンコーディングを初めて試みた際にはエラーとなるものの、Aの部分的に構築されたエンジンがキャッシュに残っているため、2回目以降のエンコーディング試行では、gobがキャッシュされたエンジンを使用してしまい、予期せぬ挙動(例えば、エラーにならずに不正なデータがエンコードされる、あるいは異なるエラーが発生するなど)を引き起こす可能性がありました。
この「一貫性のない挙動」は、デバッグを困難にし、アプリケーションの信頼性を損なうため、早急な修正が必要とされました。このコミットは、エンコーディングエンジンのコンパイルが完全に成功した場合のみキャッシュするように変更することで、この問題を解決しています。
前提知識の解説
1. encoding/gob パッケージとは
encoding/gob は、Go言語のデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するための標準パッケージです。主にGoプログラム間でGoの値を効率的にやり取りするために設計されており、RPC (Remote Procedure Call) や永続化のメカニズムとして利用されます。
gob の特徴は以下の通りです。
- 自己記述的:
gobストリームは、データだけでなく、そのデータの型情報も含まれています。これにより、受信側は事前に型を知らなくてもデータをデコードできます。 - 効率性: バイナリ形式であるため、JSONやXMLのようなテキストベースの形式よりもコンパクトで高速です。
- Go固有: Goの型システムに密接に統合されており、インターフェース、ポインタ、スライス、マップなど、Goのあらゆるデータ型を扱うことができます。
2. gob における「エクスポートされたフィールド」の重要性
Go言語では、構造体のフィールド名が大文字で始まる場合、そのフィールドは「エクスポートされている(exported)」と見なされ、パッケージ外からアクセス可能です。小文字で始まるフィールドは「エクスポートされていない(unexported)」と見なされ、そのフィールドが定義されているパッケージ内からのみアクセス可能です。
encoding/gob は、データ構造をエンコード・デコードする際に、Goの reflect パッケージを使用して構造体のフィールドにアクセスします。この際、gob はエクスポートされたフィールドのみを対象とします。これは、gob が外部に公開されるべきデータのみを扱うという設計思想に基づいています。
したがって、構造体にエクスポートされていないフィールドが含まれている場合、gob はそのフィールドをエンコード・デコードできません。通常、これはエラーとなります。このコミットで修正されたバグは、この「エクスポートされていないフィールド」がネストされた型に含まれる場合に、gob の内部的な型コンパイルとキャッシュのロジックが不完全な状態を許容してしまい、問題を引き起こしていました。
3. gob の「エンコーディングエンジン」とは
gob がGoの値をエンコードする際、内部的にはその値の型に対応する特定の「エンコーディングエンジン」を生成します。このエンジンは、特定の型を効率的にバイナリ形式に変換するためのコード(または命令セット)のようなものです。
gob は、初めて特定の型をエンコードする際に、その型の構造を解析し、エンコーディングエンジンを「コンパイル」します。このコンパイルされたエンジンは、その後の同じ型のエンコーディングのためにキャッシュされます。これにより、同じ型を繰り返しエンコードする際のオーバーヘッドが削減され、パフォーマンスが向上します。
このコミットの文脈では、「壊れたエンコーディングエンジン」とは、型定義に問題がある(例:エクスポートされていないフィールドがある)ために、gob がその型を正しくエンコードするためのエンジンを完全に構築できない状態のものを指します。
4. Issue 3273
GoのIssueトラッカーで報告されたバグです。このIssueは、encoding/gob が、エクスポートされていないフィールドを持つネストされた構造体を含む型をエンコードしようとした際に、初回はエラーになるものの、2回目以降はエラーにならない、あるいは異なるエラーになるという一貫性のない挙動を報告していました。これは、不完全なエンコーディングエンジンがキャッシュされてしまうことが原因でした。
技術的詳細
このコミットの技術的な核心は、src/pkg/encoding/gob/encode.go 内の getEncEngine 関数におけるエンコーディングエンジンのコンパイルとキャッシュのロジックの変更にあります。
getEncEngine 関数は、特定のユーザー定義型 (userTypeInfo) に対応するエンコーディングエンジン (encEngine) を取得する役割を担っています。この関数は、まずキャッシュをチェックし、エンジンが既に存在すればそれを返します。存在しない場合は、新しくエンジンをコンパイルします。
変更前のロジックでは、info.encoder = new(encEngine) で新しいエンジンオブジェクトを割り当てた後、すぐに info.encoder に代入していました。そして、その後に enc.compileEnc(ut) を呼び出して実際のコンパイルを行っていました。もし compileEnc の途中でエラーが発生した場合(例えば、ネストされた型にエクスポートされていないフィールドがあるため)、info.encoder には不完全な、あるいは壊れたエンジンオブジェクトが代入されたままになっていました。この不完全なオブジェクトがキャッシュされてしまうと、次回同じ型が要求された際に、この壊れたエンジンが返され、一貫性のない挙動を引き起こしていました。
このコミットでは、この問題を解決するために以下の変更が導入されました。
okフラグの導入:okというブール型の変数が導入され、初期値はfalseに設定されます。defer関数によるクリーンアップ:deferキーワードを使用して、getEncEngine関数が終了する際に実行される匿名関数が定義されました。このdefer関数は、okがfalseの場合(つまり、エンジンのコンパイルが成功しなかった場合)に、info.encoderをnilにリセットします。- エンジンの仮割り当て:
info.encoder = new(encEngine)で新しいエンジンオブジェクトを割り当て、info.encoderに仮代入する点は変わりません。これは、再帰的な型(例えば、自身をフィールドに持つ構造体)を扱う際に、コンパイル中に循環参照を検出できるようにするためです。 - コンパイル成功時の
okフラグ設定:enc.compileEnc(ut)による実際のエンジンコンパイルが成功した直後に、ok = trueが設定されます。
この変更により、enc.compileEnc(ut) がエラーを返したり、パニックを起こしたりして正常に完了しなかった場合、ok フラグは false のままになります。その結果、defer 関数が実行された際に info.encoder が nil にリセットされ、不完全なエンジンがキャッシュに残ることを防ぎます。これにより、gob は常に完全にコンパイルされた有効なエンジンのみをキャッシュするようになり、エンコーディングの一貫性が保証されます。
コアとなるコードの変更箇所
src/pkg/encoding/gob/encode.go
--- a/src/pkg/encoding/gob/encode.go
+++ b/src/pkg/encoding/gob/encode.go
@@ -704,9 +704,20 @@ func (enc *Encoder) getEncEngine(ut *userTypeInfo) *encEngine {
// error_ is fatal.
if err1 := enc.error_(err); err1 != nil {
error_(err1)
}
if info.encoder == nil {
- // mark this engine as underway before compiling to handle recursive types.
+ // Assign the encEngine now, so recursive types work correctly. But...
info.encoder = new(encEngine)
+ // ... if we fail to complete building the engine, don't cache the half-built machine.
+ // Doing this here means we won't cache a type that is itself OK but
+ // that contains a nested type that won't compile. The result is consistent
+ // error behavior when Encode is called multiple times on the top-level type.
+ ok := false
+ defer func() {
+ if !ok {
+ info.encoder = nil
+ }
+ }()
info.encoder = enc.compileEnc(ut)
+ ok = true
}
return info.encoder
}
src/pkg/encoding/gob/encoder_test.go
--- a/src/pkg/encoding/gob/encoder_test.go
+++ b/src/pkg/encoding/gob/encoder_test.go
@@ -780,3 +780,36 @@ func TestNilPointerInsideInterface(t *testing.T) {
t.Fatal("expected error about nil pointer and interface, got:", errMsg)
}
}
+
+type Bug4Public struct {
+ Name string
+ Secret Bug4Secret
+}
+
+type Bug4Secret struct {
+ a int // error: no exported fields.
+}
+
+// Test that a failed compilation doesn't leave around an executable encoder.
+// Issue 3273.
+func TestMutipleEncodingsOfBadType(t *testing.T) {
+ x := Bug4Public{
+ Name: "name",
+ Secret: Bug4Secret{1},
+ }
+ buf := new(bytes.Buffer)
+ enc := NewEncoder(buf)
+ err := enc.Encode(x)
+ if err == nil {
+ t.Fatal("first encoding: expected error")
+ }
+ buf.Reset()
+ enc = NewEncoder(buf)
+ err = enc.Encode(x)
+ if err == nil {
+ t.Fatal("second encoding: expected error")
+ }
+ if !strings.Contains(err.Error(), "no exported fields") {
+ t.Errorf("expected error about no exported fields; got %v", err)
+ }
+}
コアとなるコードの解説
src/pkg/encoding/gob/encode.go の変更点
このファイルでは、getEncEngine 関数内のロジックが変更されています。
-
変更前:
if info.encoder == nil { // mark this engine as underway before compiling to handle recursive types. info.encoder = new(encEngine) info.encoder = enc.compileEnc(ut) }ここでは、
info.encoderに新しいencEngineを割り当てた後、すぐにcompileEncを呼び出しています。もしcompileEncが失敗した場合、info.encoderには不完全なエンジンが残ったままになり、それがキャッシュされてしまう可能性がありました。 -
変更後:
if info.encoder == nil { // Assign the encEngine now, so recursive types work correctly. But... info.encoder = new(encEngine) // ... if we fail to complete building the engine, don't cache the half-built machine. // Doing this here means we won't cache a type that is itself OK but // that contains a nested type that won't compile. The result is consistent // error behavior when Encode is called multiple times on the top-level type. ok := false defer func() { if !ok { info.encoder = nil } }() info.encoder = enc.compileEnc(ut) ok = true }ok := false: エンジンコンパイルの成功を示すフラグを初期化します。defer func() { ... }(): 遅延実行される匿名関数を定義します。この関数はgetEncEngineがリターンする直前に実行されます。if !ok { info.encoder = nil }: もしokがfalseのまま(つまり、コンパイルが成功しなかった)であれば、info.encoderをnilにリセットします。これにより、不完全なエンジンがキャッシュされるのを防ぎます。
info.encoder = enc.compileEnc(ut): 実際のエンジンコンパイルを実行します。ok = true:compileEncがエラーなく完了した場合、okをtrueに設定します。これにより、defer関数が実行されてもinfo.encoderはnilにリセットされず、完全にコンパイルされたエンジンがキャッシュに残ります。
この変更により、gob は、エンコーディングエンジンのコンパイルが完全に成功した場合にのみ、そのエンジンをキャッシュするようになります。これにより、ネストされた不正な型が存在する場合でも、一貫してエラーを返すようになり、デバッグが容易になります。
src/pkg/encoding/gob/encoder_test.go の変更点
新しいテストケース TestMutipleEncodingsOfBadType が追加されました。このテストは、Issue 3273で報告されたバグを再現し、修正が正しく機能することを確認します。
-
Bug4Public構造体:type Bug4Public struct { Name string Secret Bug4Secret }これは、
gobでエンコード可能な公開フィールドNameと、ネストされた型Secretを持つ構造体です。 -
Bug4Secret構造体:type Bug4Secret struct { a int // error: no exported fields. }この構造体は、エクスポートされていないフィールド
aを持っています。gobはエクスポートされていないフィールドをエンコードできないため、この型はgobでエンコードしようとするとエラーになります。 -
テストロジック:
Bug4Publicのインスタンスxを作成し、SecretフィールドにBug4Secretのインスタンスを割り当てます。enc.Encode(x)を初回実行します。この際、Bug4Secretのためにエンコーディングエンジンを構築しようとしますが、エクスポートされていないフィールドがあるため失敗し、エラーが返されることを期待します (if err == nil { t.Fatal(...) })。- バッファをリセットし、新しいエンコーダを作成します。
enc.Encode(x)を2回目実行します。修正前は、ここでエラーにならない可能性がありましたが、修正後は初回と同様にエラーが返されることを期待します (if err == nil { t.Fatal(...) })。- 返されたエラーメッセージが「no exported fields」を含むことを確認します (
if !strings.Contains(err.Error(), "no exported fields") { t.Errorf(...) })。
このテストは、gob が不完全なエンコーディングエンジンをキャッシュせず、常に正しいエラーを返すことを保証します。
関連リンク
- Go Issue 3273: https://go.dev/issue/3273
- Go CL 6294067: https://golang.org/cl/6294067
参考にした情報源リンク
- Go Issue Tracker: https://go.dev/issue
- Go
encoding/gobパッケージドキュメント: https://pkg.go.dev/encoding/gob - Go
reflectパッケージドキュメント: https://pkg.go.dev/reflect - Go言語のexported/unexported fieldsに関する情報 (一般的なGoのドキュメントやチュートリアル)