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

[インデックス 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 でエンコードできない場合(例えば、エクスポートされていないフィールドを持つ構造体など)、エンコーディングはエラーとなるべきです。しかし、このバグが存在する状況では、以下のような問題が発生していました。

  1. ネストされた不正な型: ある構造体 A が、gob でエンコードできない別の構造体 B をフィールドとして持っている場合。
  2. 部分的なエンジン構築とキャッシュ: gobA のエンコーディングエンジンを構築しようとすると、その過程で B のエンコーディングエンジンも構築しようとします。もし B の構築が失敗した場合でも、A のエンジンが部分的に構築された状態でキャッシュされてしまう可能性がありました。
  3. 一貫性のない挙動: その結果、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 には不完全な、あるいは壊れたエンジンオブジェクトが代入されたままになっていました。この不完全なオブジェクトがキャッシュされてしまうと、次回同じ型が要求された際に、この壊れたエンジンが返され、一貫性のない挙動を引き起こしていました。

このコミットでは、この問題を解決するために以下の変更が導入されました。

  1. ok フラグの導入: ok というブール型の変数が導入され、初期値は false に設定されます。
  2. defer 関数によるクリーンアップ: defer キーワードを使用して、getEncEngine 関数が終了する際に実行される匿名関数が定義されました。この defer 関数は、okfalse の場合(つまり、エンジンのコンパイルが成功しなかった場合)に、info.encodernil にリセットします。
  3. エンジンの仮割り当て: info.encoder = new(encEngine) で新しいエンジンオブジェクトを割り当て、info.encoder に仮代入する点は変わりません。これは、再帰的な型(例えば、自身をフィールドに持つ構造体)を扱う際に、コンパイル中に循環参照を検出できるようにするためです。
  4. コンパイル成功時の ok フラグ設定: enc.compileEnc(ut) による実際のエンジンコンパイルが成功した直後に、ok = true が設定されます。

この変更により、enc.compileEnc(ut) がエラーを返したり、パニックを起こしたりして正常に完了しなかった場合、ok フラグは false のままになります。その結果、defer 関数が実行された際に info.encodernil にリセットされ、不完全なエンジンがキャッシュに残ることを防ぎます。これにより、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
    }
    
    1. ok := false: エンジンコンパイルの成功を示すフラグを初期化します。
    2. defer func() { ... }(): 遅延実行される匿名関数を定義します。この関数は getEncEngine がリターンする直前に実行されます。
      • if !ok { info.encoder = nil }: もし okfalse のまま(つまり、コンパイルが成功しなかった)であれば、info.encodernil にリセットします。これにより、不完全なエンジンがキャッシュされるのを防ぎます。
    3. info.encoder = enc.compileEnc(ut): 実際のエンジンコンパイルを実行します。
    4. ok = true: compileEnc がエラーなく完了した場合、oktrue に設定します。これにより、defer 関数が実行されても info.encodernil にリセットされず、完全にコンパイルされたエンジンがキャッシュに残ります。

この変更により、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 でエンコードしようとするとエラーになります。

  • テストロジック:

    1. Bug4Public のインスタンス x を作成し、Secret フィールドに Bug4Secret のインスタンスを割り当てます。
    2. enc.Encode(x)初回実行します。この際、Bug4Secret のためにエンコーディングエンジンを構築しようとしますが、エクスポートされていないフィールドがあるため失敗し、エラーが返されることを期待します (if err == nil { t.Fatal(...) })。
    3. バッファをリセットし、新しいエンコーダを作成します。
    4. enc.Encode(x)2回目実行します。修正前は、ここでエラーにならない可能性がありましたが、修正後は初回と同様にエラーが返されることを期待します (if err == nil { t.Fatal(...) })。
    5. 返されたエラーメッセージが「no exported fields」を含むことを確認します (if !strings.Contains(err.Error(), "no exported fields") { t.Errorf(...) })。

このテストは、gob が不完全なエンコーディングエンジンをキャッシュせず、常に正しいエラーを返すことを保証します。

関連リンク

参考にした情報源リンク