[インデックス 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のドキュメントやチュートリアル)