[インデックス 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のstruct
、slice
、map
、プリミティブ型などを直接エンコード・デコードできます。 GobEncoder
とGobDecoder
インターフェース: ユーザーは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.isGobEncoder
が true
の場合(つまり、型が GobEncoder
を実装している場合)、sendActualType
関数に ut.user
を渡していました。ここで ut
は userType
構造体であり、ut.user
は GobEncoder
インターフェースを実装している「ユーザーが定義した型」そのものを指します。
問題は、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 が修正されました。
新しいテストケース TestGobEncoderValueThenPointer
と TestGobEncoderPointerThenValue
は、この修正が正しく機能することを確認するために追加されました。これらのテストは、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.go
の sendType
関数内の変更は非常に小さいですが、その影響は大きいです。
- 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
の変更
このファイルには、バグ修正を検証するための新しいテストケースが追加されています。
-
新しい構造体
GobTest6
とGobTest7
の追加: これらの構造体は、ValueGobber
型(GobEncoder
を実装)を、値型 (V ValueGobber
) とポインタ型 (W *ValueGobber
) の両方でフィールドとして持つように設計されています。GobTest6
:V
が値型、W
がポインタ型。GobTest7
:V
がポインタ型、W
が値型。 これらの構造体は、同じGobEncoder
型が異なる「間接性」(値とポインタ)で同時に使用されるシナリオをシミュレートします。
-
TestGobEncoderValueThenPointer
関数の追加: このテストは、GobTest6
のインスタンスをエンコード・デコードします。これは、GobEncoder
を実装する型が、値型としてエンコードされた後にポインタ型としてエンコードされるシナリオをテストします。変更前のコードでは、この順序でエンコードすると型定義が重複して送信されるバグがありました。このテストは、修正後にこの問題が発生しないことを確認します。 -
TestGobEncoderPointerThenValue
関数の追加: このテストは、GobTest7
のインスタンスをエンコード・デコードします。これは、GobEncoder
を実装する型が、ポインタ型としてエンコードされた後に値型としてエンコードされるシナリオをテストします。このテストも、異なる順序でのエンコーディングで型定義の重複が発生しないことを確認します。
これらのテストは、bytes.Buffer
を使用して gob
エンコーダとデコーダを作成し、GobTest6
または GobTest7
のインスタンスをエンコード・デコードし、元の値とデコードされた値が一致するかどうかを検証します。これにより、gob
が GobEncoder
を実装する型の値とポインタの両方を正しく処理し、型定義の重複なしにエンコード・デコードできることが保証されます。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/bc1152a32e2bfc5e80819034930998e44b62f0fe
- Go Issue #4647: https://golang.org/issue/4647
- Gerrit Change List 7085051: https://golang.org/cl/7085051
参考にした情報源リンク
- Go Programming Language Specification - The
reflect
package: https://pkg.go.dev/reflect - Go Programming Language Specification - The
encoding/gob
package: https://pkg.go.dev/encoding/gob - Go Blog - The Go Blog: Gobs of data: https://go.dev/blog/gobs-of-data
- Go言語のreflectパッケージについて: https://zenn.dev/hsaki/articles/go-reflect-package (日本語の参考情報)
- Go言語のポインタについて: https://go.dev/tour/moretypes/1 (Go公式チュートリアル)
- Go言語のインターフェースについて: https://go.dev/tour/methods/9 (Go公式チュートリアル)
- Go言語の構造体について: https://go.dev/tour/moretypes/2 (Go公式チュートリアル)
- Go言語のテストについて: https://go.dev/doc/tutorial/add-a-test (Go公式チュートリアル)
- Go言語の
bytes
パッケージについて: https://pkg.go.dev/bytesI have generated the comprehensive technical explanation in Markdown format, adhering to all specified instructions and the required chapter structure. The output is provided below.