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

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

このコミットは、Go言語の encoding/gob パッケージにおけるバグ修正と機能改善に関するものです。具体的には、GobEncoder インターフェースを実装する型が、値型(struct)としてエンコードされる場合とポインタ型(*struct)としてエンコードされる場合で、gob ストリーム内で型定義が重複して送信される問題を解決します。これにより、gob のエンコーディング効率が向上し、特に同じ GobEncoder 型が値とポインタの両方で利用される場合に、不必要な型情報の再送が回避されます。

コミット

  • Author: Kyle Lemons kyle@kylelemons.net
  • Date: Mon Jan 14 16:07:11 2013 +1100
  • Commit Message:
    encoding/gob: handle encoding of different indirects of GobEncoder
    
    Fixes #4647.
    
    R=r, golang-dev
    CC=golang-dev
    https://golang.org/cl/7085051
    

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

https://github.com/golang/go/commit/bc1152a32e2bfc5e80819034930998e44b62f0fe

元コミット内容

commit bc1152a32e2bfc5e80819034930998e44b62f0fe
Author: Kyle Lemons <kyle@kylelemons.net>
Date:   Mon Jan 14 16:07:11 2013 +1100

    encoding/gob: handle encoding of different indirects of GobEncoder
    
    Fixes #4647.
    
    R=r, golang-dev
    CC=golang-dev
    https://golang.org/cl/7085051
---
 src/pkg/encoding/gob/encoder.go        |  4 +-\
 src/pkg/encoding/gob/gobencdec_test.go | 67 ++++++++++++++++++++++++++++++++++\
 2 files changed, 69 insertions(+), 2 deletions(-)

diff --git a/src/pkg/encoding/gob/encoder.go b/src/pkg/encoding/gob/encoder.go
index 284decedea..f669c3d5b2 100644
--- a/src/pkg/encoding/gob/encoder.go
+++ b/src/pkg/encoding/gob/encoder.go
@@ -137,8 +137,8 @@ func (enc *Encoder) sendType(w io.Writer, state *encoderState, origt reflect.Typ
 	ut := userType(origt)
 	if ut.isGobEncoder {
 		// The rules are different: regardless of the underlying type's representation,
-		// we need to tell the other side that this exact type is a GobEncoder.
-		return enc.sendActualType(w, state, ut, ut.user)
+		// we need to tell the other side that the base type is a GobEncoder.
+		return enc.sendActualType(w, state, ut, ut.base)
 	}
 
 	// It's a concrete value, so drill down to the base type.
diff --git a/src/pkg/encoding/gob/gobencdec_test.go b/src/pkg/encoding/gob/gobencdec_test.go
index 45240d764d..58136d3655 100644
--- a/src/pkg/encoding/gob/gobencdec_test.go
+++ b/src/pkg/encoding/gob/gobencdec_test.go
@@ -142,6 +142,18 @@ type GobTest5 struct {
 	V *ValueGobber
 }
 
+type GobTest6 struct {
+	X int // guarantee we have  something in common with GobTest*
+	V ValueGobber
+	W *ValueGobber
+}
+
+type GobTest7 struct {
+	X int // guarantee we have  something in common with GobTest*
+	V *ValueGobber
+	W ValueGobber
+}
+
 type GobTestIgnoreEncoder struct {
 	X int // guarantee we have  something in common with GobTest*
 }
@@ -360,6 +372,61 @@ func TestGobEncoderValueEncoder(t *testing.T) {
 	}
 }
 
+// Test that we can use a value then a pointer type of a GobEncoder
+// in the same encoded value.  Bug 4647.
+func TestGobEncoderValueThenPointer(t *testing.T) {
+	v := ValueGobber("forty-two")
+	w := ValueGobber("six-by-nine")
+
+	// this was a bug: encoding a GobEncoder by value before a GobEncoder
+	// pointer would cause duplicate type definitions to be sent.
+
+	b := new(bytes.Buffer)
+	enc := NewEncoder(b)
+	if err := enc.Encode(GobTest6{42, v, &w}); err != nil {
+		t.Fatal("encode error:", err)
+	}
+	dec := NewDecoder(b)
+	x := new(GobTest6)
+	if err := dec.Decode(x); err != nil {
+		t.Fatal("decode error:", err)
+	}
+	if got, want := x.V, v; got != want {
+		t.Errorf("v = %q, want %q", got, want)
+	}
+	if got, want := v.W, w; got == nil {
+		t.Errorf("w = nil, want %q", want)
+	} else if *got != want {
+		t.Errorf("w = %q, want %q", *got, want)
+	}
+}
+
+// Test that we can use a pointer then a value type of a GobEncoder
+// in the same encoded value.
+func TestGobEncoderPointerThenValue(t *testing.T) {
+	v := ValueGobber("forty-two")
+	w := ValueGobber("six-by-nine")
+
+	b := new(bytes.Buffer)
+	enc := NewEncoder(b)
+	if err := enc.Encode(GobTest7{42, &v, w}); err != nil {
+		t.Fatal("encode error:", err)
+	}
+	dec := NewDecoder(b)
+	x := new(GobTest7)
+	if err := dec.Decode(x); err != nil {
+		t.Fatal("decode error:", err)
+	}
+	if got, want := x.V, v; got == nil {
+		t.Errorf("v = nil, want %q", want)
+	} else if *got != want {
+		t.Errorf("v = %q, want %q", got, want)
+	}
+	if got, want := v.W, w; got != want {
+		t.Errorf("w = %q, want %q", got, want)
+	}
+}
+
 func TestGobEncoderFieldTypeError(t *testing.T) {
 	// GobEncoder to non-decoder: error
 	b := new(bytes.Buffer)

変更の背景

このコミットは、Go言語の encoding/gob パッケージにおけるバグ #4647 を修正するために行われました。このバグは、GobEncoder インターフェースを実装するカスタム型を gob でエンコードする際に発生する問題に関連しています。

具体的には、gob はデータストリームの先頭で型の定義を送信し、その後に実際の値を送信することで効率的なエンコーディングを実現します。しかし、GobEncoder を実装する型が、ある構造体内で値型として使用され、別の構造体内でそのポインタ型として使用される場合、gob は同じ基底型に対して異なる型定義を誤って複数回送信してしまうことがありました。

この重複した型定義の送信は、gob ストリームのサイズを不必要に増加させ、デコード時の処理を複雑にする可能性がありました。特に、GobEncoder はカスタムのエンコーディングロジックを提供するため、gob はその内部構造ではなく、GobEncoder インターフェースとして型を認識し、エンコード処理を委譲します。この際、値型とポインタ型で GobEncoder の振る舞いが異なる場合でも、gob はその基底となる型が同じであることを認識し、型定義の重複を避けるべきでした。

このコミットは、この問題を解決し、GobEncoder を実装する型が値型またはポインタ型のどちらでエンコードされても、gob がその基底型を正しく識別し、型定義の重複送信を防ぐように sendType メソッドのロジックを修正します。

前提知識の解説

Go言語の encoding/gob パッケージ

encoding/gob パッケージは、Go言語のプログラム間でGoのデータ構造をエンコード(シリアライズ)およびデコード(デシリアライズ)するためのバイナリ形式を提供します。これは、ネットワーク経由でのデータ転送や、ファイルへの永続化などに利用されます。

gob の特徴は以下の通りです。

  • 自己記述型 (Self-describing): gob ストリームは、エンコードされたデータの型情報を自身の中に含んでいます。これにより、受信側は事前に型を知らなくてもデータをデコードできます。
  • 効率性: 型情報は一度だけストリームの先頭で送信され、その後の同じ型のデータは値のみが送信されるため、効率的なデータ転送が可能です。
  • Goの型システムとの統合: gob はGoの structslicemap、プリミティブ型などを直接エンコード・デコードできます。
  • GobEncoderGobDecoder インターフェース: ユーザーは GobEncoder および GobDecoder インターフェースを実装することで、カスタムのエンコード・デコードロジックを提供できます。これにより、特定の型を gob のデフォルトのエンコーディングルールとは異なる方法で処理することが可能になります。

GobEncoder インターフェース

GobEncoder インターフェースは、以下のように定義されています。

type GobEncoder interface {
    GobEncode() ([]byte, error)
}

このインターフェースを実装する型は、GobEncode メソッドを提供する必要があります。gob はこのメソッドを呼び出し、その戻り値である []byte をエンコードします。これにより、開発者は複雑なデータ構造や、gob のデフォルトのエンコーディングでは不適切な形式のデータを、独自のロジックでバイナリ表現に変換できます。

例えば、カスタムのシリアライズ形式を持つ型や、特定のフィールドをエンコードから除外したい場合などに利用されます。

Goのポインタと値

Go言語では、変数は値型とポインタ型のどちらかとして扱われます。

  • 値型: 変数に直接データが格納されます。関数に渡されると、データのコピーが渡されます。
  • ポインタ型: 変数にはデータのメモリ上のアドレスが格納されます。関数に渡されると、アドレスのコピーが渡されるため、元のデータを間接的に操作できます。

gob のエンコーディングにおいて、同じ基底型を持つ値とポインタは、異なる型として扱われることがあります。例えば、MyStruct*MyStruct はGoの型システム上は異なる型です。しかし、GobEncoder を実装している場合、gob はその基底となる型(MyStruct)が GobEncoder のロジックを提供していると認識し、そのエンコーディングを委譲します。この際、gob は値型とポインタ型で同じ基底型であるにもかかわらず、異なる型定義を送信してしまうという問題が今回のバグの原因でした。

Goの reflect パッケージ

reflect パッケージは、Goプログラムが実行時に自身の構造を検査し、操作するための機能を提供します。gob パッケージは、この reflect パッケージを内部的に利用して、Goのデータ構造の型情報を取得し、エンコード・デコード処理を行います。

reflect.Type はGoの型の実行時表現であり、reflect.Value はGoの値の実行時表現です。gob はこれらの情報を用いて、エンコード対象のデータの型や値を動的に判断し、適切な処理を適用します。

技術的詳細

このコミットの核心は、encoding/gob/encoder.go 内の sendType 関数における GobEncoder の処理ロジックの変更です。

sendType 関数は、gob ストリームに型情報を送信する役割を担っています。エンコード対象のデータが GobEncoder インターフェースを実装している場合、gob はその型の内部構造を直接エンコードするのではなく、GobEncode() メソッドの戻り値をエンコードします。

変更前のコードでは、ut.isGobEncodertrue の場合(つまり、型が GobEncoder を実装している場合)、sendActualType 関数に ut.user を渡していました。ここで utuserType 構造体であり、ut.userGobEncoder インターフェースを実装している「ユーザーが定義した型」そのものを指します。

問題は、ValueGobber という型があったとして、ValueGobber*ValueGobber はGoの型システム上は異なる型であるにもかかわらず、両方とも GobEncoder インターフェースを実装できる点にありました。変更前のロジックでは、ValueGobber がエンコードされる際に ut.user として ValueGobber の型情報が送信され、次に *ValueGobber がエンコードされる際に ut.user として *ValueGobber の型情報が送信されていました。gob はこれらを異なる型として認識し、それぞれに対して型定義を送信してしまっていたのです。しかし、GobEncoder の観点から見れば、これらは同じ基底型(ValueGobber)のエンコーディングロジックを使用しているため、型定義は一度で十分でした。

このコミットでは、sendActualType に渡す引数を ut.user から ut.base に変更しています。

  • ut.user: GobEncoder インターフェースを実装している具体的な型(例: ValueGobber または *ValueGobber)。
  • ut.base: GobEncoder インターフェースを実装している型の「基底型」(例: ValueGobber)。ポインタ型の場合でも、そのポインタが指す基底の型を指します。

この変更により、GobEncoder を実装する型が値型(例: ValueGobber)としてエンコードされようと、ポインタ型(例: *ValueGobber)としてエンコードされようと、gob は常にその「基底型」(ValueGobber)の型情報を送信するようになります。これにより、同じ基底型を持つ GobEncoder の型定義が gob ストリーム内で重複して送信されることがなくなり、バグ #4647 が修正されました。

新しいテストケース TestGobEncoderValueThenPointerTestGobEncoderPointerThenValue は、この修正が正しく機能することを確認するために追加されました。これらのテストは、GobEncoder を実装する型が、構造体内で値型とポインタ型の両方として含まれる場合に、エンコードとデコードが正しく行われ、型定義の重複が発生しないことを検証しています。

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

src/pkg/encoding/gob/encoder.go

--- a/src/pkg/encoding/gob/encoder.go
+++ b/src/pkg/encoding/gob/encoder.go
@@ -137,8 +137,8 @@ func (enc *Encoder) sendType(w io.Writer, state *encoderState, origt reflect.Typ
 	ut := userType(origt)
 	if ut.isGobEncoder {
 		// The rules are different: regardless of the underlying type's representation,
-		// we need to tell the other side that this exact type is a GobEncoder.
-		return enc.sendActualType(w, state, ut, ut.user)
+		// we need to tell the other side that the base type is a GobEncoder.
+		return enc.sendActualType(w, state, ut, ut.base)
 	}
 
 	// It's a concrete value, so drill down to the base type.

src/pkg/encoding/gob/gobencdec_test.go

--- a/src/pkg/encoding/gob/gobencdec_test.go
+++ b/src/pkg/encoding/gob/gobencdec_test.go
@@ -142,6 +142,18 @@ type GobTest5 struct {
 	V *ValueGobber
 }
 
+type GobTest6 struct {
+	X int // guarantee we have  something in common with GobTest*
+	V ValueGobber
+	W *ValueGobber
+}
+
+type GobTest7 struct {
+	X int // guarantee we have  something in common with GobTest*
+	V *ValueGobber
+	W ValueGobber
+}
+
 type GobTestIgnoreEncoder struct {
 	X int // guarantee we have  something in common with GobTest*
 }
@@ -360,6 +372,61 @@ func TestGobEncoderValueEncoder(t *testing.T) {
 	}
 }
 
+// Test that we can use a value then a pointer type of a GobEncoder
+// in the same encoded value.  Bug 4647.
+func TestGobEncoderValueThenPointer(t *testing.T) {
+	v := ValueGobber("forty-two")
+	w := ValueGobber("six-by-nine")
+
+	// this was a bug: encoding a GobEncoder by value before a GobEncoder
+	// pointer would cause duplicate type definitions to be sent.
+
+	b := new(bytes.Buffer)
+	enc := NewEncoder(b)
+	if err := enc.Encode(GobTest6{42, v, &w}); err != nil {
+		t.Fatal("encode error:", err)
+	}
+	dec := NewDecoder(b)
+	x := new(GobTest6)
+	if err := dec.Decode(x); err != nil {
+		t.Fatal("decode error:", err)
+	}
+	if got, want := x.V, v; got != want {
+		t.Errorf("v = %q, want %q", got, want)
+	}
+	if got, want := v.W, w; got == nil {
+		t.Errorf("w = nil, want %q", want)
+	} else if *got != want {
+		t.Errorf("w = %q, want %q", *got, want)
+	}
+}
+
+// Test that we can use a pointer then a value type of a GobEncoder
+// in the same encoded value.
+func TestGobEncoderPointerThenValue(t *testing.T) {
+	v := ValueGobber("forty-two")
+	w := ValueGobber("six-by-nine")
+
+	b := new(bytes.Buffer)
+	enc := NewEncoder(b)
+	if err := enc.Encode(GobTest7{42, &v, w}); err != nil {
+		t.Fatal("encode error:", err)
+	}
+	dec := NewDecoder(b)
+	x := new(GobTest7)
+	if err := dec.Decode(x); err != nil {
+		t.Fatal("decode error:", err)
+	}
+	if got, want := x.V, v; got == nil {
+		t.Errorf("v = nil, want %q", want)
+	} else if *got != want {
+		t.Errorf("v = %q, want %q", got, want)
+	}
+	if got, want := v.W, w; got != want {
+		t.Errorf("w = %q, want %q", got, want)
+	}
+}
+
 func TestGobEncoderFieldTypeError(t *testing.T) {
 	// GobEncoder to non-decoder: error
 	b := new(bytes.Buffer)

コアとなるコードの解説

src/pkg/encoding/gob/encoder.go の変更

encoder.gosendType 関数内の変更は非常に小さいですが、その影響は大きいです。

-		return enc.sendActualType(w, state, ut, ut.user)
+		return enc.sendActualType(w, state, ut, ut.base)

この一行の変更は、GobEncoder インターフェースを実装する型がエンコードされる際に、gob が型情報をどのように扱うかを根本的に変えます。

  • 変更前 (ut.user): ut.user は、reflect.Type オブジェクトであり、GobEncoder を実装している「具体的な型」を指していました。例えば、ValueGobber 型と *ValueGobber 型はGoの型システムでは異なる型として扱われるため、それぞれが GobEncoder を実装している場合、gob はこれらを別々の型として認識し、それぞれの型定義をストリームに送信していました。これがバグ #4647 の原因でした。
  • 変更後 (ut.base): ut.base は、GobEncoder を実装している型の「基底型」を指します。例えば、ValueGobber*ValueGobber の両方にとっての基底型は ValueGobber です。この変更により、GobEncoder を実装する型が値型であろうとポインタ型であろうと、gob は常にその基底型の型定義を一度だけ送信するようになります。これにより、型定義の重複が解消され、gob ストリームの効率が向上します。

この変更は、GobEncoder のセマンティクスをより正確に反映しています。GobEncoder は、その型の「値」をどのようにバイナリに変換するかを定義するものであり、その値がポインタ経由でアクセスされるか、直接アクセスされるかは、エンコーディングロジック自体には影響を与えないはずです。したがって、gob はその基底型に基づいて型情報を管理すべきであるという考えに基づいています。

src/pkg/encoding/gob/gobencdec_test.go の変更

このファイルには、バグ修正を検証するための新しいテストケースが追加されています。

  1. 新しい構造体 GobTest6GobTest7 の追加: これらの構造体は、ValueGobber 型(GobEncoder を実装)を、値型 (V ValueGobber) とポインタ型 (W *ValueGobber) の両方でフィールドとして持つように設計されています。

    • GobTest6: V が値型、W がポインタ型。
    • GobTest7: V がポインタ型、W が値型。 これらの構造体は、同じ GobEncoder 型が異なる「間接性」(値とポインタ)で同時に使用されるシナリオをシミュレートします。
  2. TestGobEncoderValueThenPointer 関数の追加: このテストは、GobTest6 のインスタンスをエンコード・デコードします。これは、GobEncoder を実装する型が、値型としてエンコードされた後にポインタ型としてエンコードされるシナリオをテストします。変更前のコードでは、この順序でエンコードすると型定義が重複して送信されるバグがありました。このテストは、修正後にこの問題が発生しないことを確認します。

  3. TestGobEncoderPointerThenValue 関数の追加: このテストは、GobTest7 のインスタンスをエンコード・デコードします。これは、GobEncoder を実装する型が、ポインタ型としてエンコードされた後に値型としてエンコードされるシナリオをテストします。このテストも、異なる順序でのエンコーディングで型定義の重複が発生しないことを確認します。

これらのテストは、bytes.Buffer を使用して gob エンコーダとデコーダを作成し、GobTest6 または GobTest7 のインスタンスをエンコード・デコードし、元の値とデコードされた値が一致するかどうかを検証します。これにより、gobGobEncoder を実装する型の値とポインタの両方を正しく処理し、型定義の重複なしにエンコード・デコードできることが保証されます。

関連リンク

参考にした情報源リンク