[インデックス 12022] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/gob
パッケージにおけるバグ修正に関するものです。encoding/gob
は、Goのデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのパッケージであり、特にGoプログラム間でのデータ交換や永続化に利用されます。
この修正では、encoding/gob
の型システム、特に再帰的なデータ構造の型定義とID割り当てを扱う src/pkg/encoding/gob/type.go
と、その問題を再現し修正を検証するためのテストケースが追加された src/pkg/encoding/gob/encoder_test.go
が変更されています。
コミット
encoding/gob
パッケージにおいて、相互再帰的な構造体のスライスをエンコードする際のバグを修正します。この修正は、型を構築する際に要素の型IDがまだ割り当てられていない(ゼロである)場合に、そのIDを明示的に設定することで行われます。本来であれば、型IDの設定シーケンスを根本的に変更することでより良い修正が可能でしたが、それは既存のバイナリ互換性を損なうため、このコミットでは互換性を維持しつつ問題を解決するアプローチが採用されました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/793f6f3cc3c2e6a5fc6636f984eadb808c7b62e8
元コミット内容
commit 793f6f3cc3c2e6a5fc6636f984eadb808c7b62e8
Author: Rob Pike <r@golang.org>
Date: Sat Feb 18 12:43:08 2012 +1100
encoding/gob: fix mutually recursive slices of structs
Fix by setting the element type if we discover it's zero while building.
We could have fixed this better with foresight by doing the id setting in a
different sequence, but doing that now would break binary compatibility.
Fixes #2995.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5675083
変更の背景
このコミットは、Goのencoding/gob
パッケージが、相互に再帰する構造体のスライス(例: []*MyStruct
の MyStruct
が自身へのポインタを持つ場合)を正しくエンコード/デコードできないというバグ(Issue 2995)を修正するために行われました。
gob
はデータをシリアライズする際に、各型に一意のIDを割り当て、そのIDを使ってデータの構造を表現します。再帰的な型、特に構造体やスライスが自身を参照するような場合、型IDの割り当て順序やタイミングが重要になります。問題は、相互再帰的な型を処理する際に、ある型の要素の型IDがまだ割り当てられていない(ゼロ値のまま)状態で参照されてしまうことが原因でした。これにより、エンコード時に型情報が不完全になり、デコード時にエラーが発生したり、データが破損したりする可能性がありました。
コミットメッセージにある「We could have fixed this better with foresight by doing the id setting in a different sequence, but doing that now would break binary compatibility.」という記述は、この問題の根本的な解決には型IDの割り当てロジック全体の見直しが必要であったものの、それが既存のgob
形式とのバイナリ互換性を破壊してしまうため、より限定的かつ互換性を維持する形での修正が選択されたことを示しています。
前提知識の解説
Goのencoding/gob
パッケージ
encoding/gob
は、Goプログラム間でGoのデータ構造を効率的にシリアライズおよびデシリアライズするためのパッケージです。ネットワーク経由でのデータ転送や、ファイルへの永続化などに利用されます。gob
は、エンコードするデータの型情報を自動的に登録し、その型情報に基づいてデータをコンパクトなバイナリ形式に変換します。この型情報は、エンコードされたデータストリームの先頭に一度だけ書き込まれ、デコード側でその型情報を使ってデータを再構築します。
Goの型システムとリフレクション
Goは静的型付け言語ですが、encoding/gob
のようなパッケージは実行時に型の情報を動的に扱う必要があります。これを可能にするのがGoのreflect
パッケージです。reflect.Type
は、Goのあらゆる型の実行時表現を提供し、型の名前、フィールド、メソッドなどの情報を取得できます。gob
はreflect
パッケージを利用して、エンコード/デコード対象のGoのデータ構造の型を分析し、それに対応するgob
の型表現を構築します。
再帰的なデータ構造
再帰的なデータ構造とは、その定義の中に自分自身、または自分自身を含む別の型への参照を持つデータ構造のことです。 例:
- 自己参照型:
type Node struct { Value int; Next *Node }
(リンクリストのノードなど) - 相互参照型:
type A struct { B *B }; type B struct { A *A }
(AがBを参照し、BがAを参照する)
このコミットで問題となったのは、特に「相互再帰的な構造体のスライス」です。例えば、type Bug3 struct { Num int; Children []*Bug3 }
のように、Bug3
構造体がBug3
へのポインタのスライス[]*Bug3
を持つ場合です。このような構造をgob
で処理する際、型IDの割り当て順序が複雑になり、問題が発生することがありました。
Goのポインタとスライス
Goでは、スライスは基盤となる配列への参照と長さ、容量を持つデータ構造です。ポインタはメモリ上の特定のアドレスを指します。[]*Bug3
のような型は、「Bug3
構造体へのポインタのスライス」を意味します。gob
はポインタをデリファレンスしてその先の値をエンコードしますが、再帰的なポインタ構造の場合、無限ループや型情報の不完全な登録といった問題を引き起こす可能性があります。
バイナリ互換性
ソフトウェア開発において、バイナリ互換性とは、新しいバージョンのソフトウェアが古いバージョンのデータ形式やコンパイル済みコードと問題なく連携できる能力を指します。encoding/gob
の場合、一度エンコードされたgob
データは、将来のバージョンのgob
デコーダでも正しくデコードできる必要があります。このコミットでは、型IDの割り当てロジックを根本的に変更すると、過去にエンコードされたgob
データがデコードできなくなる可能性があったため、既存のバイナリ互換性を維持する形での修正が優先されました。
技術的詳細
encoding/gob
パッケージは、エンコード対象のGoの型を内部的なgobType
表現に変換し、それぞれに一意のtypeId
を割り当てます。このtypeId
は、エンコードされたデータストリーム内で型を識別するために使用されます。
問題の核心は、再帰的な型(特にスライスや構造体)を処理する際のtypeId
の割り当てタイミングにありました。gob
が型を解析し、その内部表現を構築する過程で、ある型がまだtypeId
が割り当てられていない(id() == 0
)別の型を参照する状況が発生しました。これは、型構築のプロセスが完了する前に、その型の一部が別の型によって参照される「前方参照」のような状況で顕著になります。
具体的には、sliceType
(スライス型)やstructType
(構造体型)の初期化中に、その要素型やフィールド型がまだtypeId
を持っていない場合がありました。gob
の型登録システムでは、setTypeId
関数が新しいtypeId
を割り当て、idToType
マップに登録します。しかし、再帰的な型の場合、setTypeId
が呼び出される前に、その型が別の場所で参照され、id() == 0
の状態のまま処理が進んでしまうことがありました。
このコミットの修正は、このid() == 0
の状態を検出し、その場でsetTypeId
を呼び出して型IDを割り当てることで、不完全な型情報が伝播するのを防ぎます。これにより、再帰的な型が正しく登録され、エンコード/デコードのプロセスが正常に完了するようになります。
コミットメッセージにある「We could have fixed this better with foresight by doing the id setting in a different sequence」とは、型構築の初期段階で常にtypeId
を割り当てるような設計にしていれば、このような問題は発生しなかったことを示唆しています。しかし、その変更は既存のgob
形式のバイナリ互換性を破壊するため、id() == 0
のチェックと条件付きのsetTypeId
呼び出しという、より局所的な修正が採用されました。
コアとなるコードの変更箇所
src/pkg/encoding/gob/encoder_test.go
--- a/src/pkg/encoding/gob/encoder_test.go
+++ b/src/pkg/encoding/gob/encoder_test.go
@@ -685,3 +685,30 @@ func TestSliceIncompatibility(t *testing.T) {
t.Error("expected compatibility error")
}
}
+
+// Mutually recursive slices of structs caused problems.
+type Bug3 struct {
+ Num int
+ Children []*Bug3
+}
+
+func TestGobPtrSlices(t *testing.T) {
+ in := []*Bug3{
+ &Bug3{1, nil},
+ &Bug3{2, nil},
+ }
+ b := new(bytes.Buffer)
+ err := NewEncoder(b).Encode(&in)
+ if err != nil {
+ t.Fatal("encode:", err)
+ }
+
+ var out []*Bug3
+ err = NewDecoder(b).Decode(&out)
+ if err != nil {
+ t.Fatal("decode:", err)
+ }
+ if !reflect.DeepEqual(in, out) {
+ t.Fatal("got %v; wanted %v", out, in)
+ }
+}
src/pkg/encoding/gob/type.go
--- a/src/pkg/encoding/gob/type.go
+++ b/src/pkg/encoding/gob/type.go
@@ -152,6 +152,10 @@ var idToType = make(map[typeId]gobType)
var builtinIdToType map[typeId]gobType // set in init() after builtins are established
func setTypeId(typ gobType) {
+ // When building recursive types, someone may get there before us.
+ if typ.id() != 0 {
+ return
+ }
nextId++
typ.setId(nextId)
idToType[nextId] = typ
@@ -346,6 +350,11 @@ func newSliceType(name string) *sliceType {
func (s *sliceType) init(elem gobType) {
// Set our type id before evaluating the element's, in case it's our own.
setTypeId(s)
+ // See the comments about ids in newTypeObject. Only slices and
+ // structs have mutual recursion.
+ if elem.id() == 0 {
+ setTypeId(elem)
+ }
s.Elem = elem.id()
}
@@ -503,6 +512,13 @@ func newTypeObject(name string, ut *userTypeInfo, rt reflect.Type) (gobType, err
if err != nil {
return nil, err
}
+ // Some mutually recursive types can cause us to be here while
+ // still defining the element. Fix the element type id here.
+ // We could do this more neatly by setting the id at the start of
+ // building every type, but that would break binary compatibility.
+ if gt.id() == 0 {
+ setTypeId(gt)
+ }
st.Field = append(st.Field, &fieldType{f.Name, gt.id()})
}
return st, nil
コアとなるコードの解説
src/pkg/encoding/gob/encoder_test.go
の変更
type Bug3 struct { Num int; Children []*Bug3 }
の追加: この構造体は、Children
フィールドがBug3
型へのポインタのスライスであるため、自己参照的かつスライスを含む再帰的なデータ構造の典型例です。この型が、まさにこのコミットで修正されるべきバグを再現するために設計されています。TestGobPtrSlices
関数の追加: このテスト関数は、Bug3
型のスライス([]*Bug3
)を作成し、それをgob
でエンコードし、その後デコードします。最後に、元のデータとデコードされたデータがreflect.DeepEqual
で完全に一致するかどうかを検証します。このテストが成功することで、相互再帰的な構造体のスライスがgob
で正しく処理されるようになったことが確認できます。
src/pkg/encoding/gob/type.go
の変更
-
func setTypeId(typ gobType)
内の変更:if typ.id() != 0 { return }
この変更は、
setTypeId
関数が呼び出された際に、既にtypeId
が割り当てられている(typ.id()
がゼロではない)場合は、それ以上処理を行わずに早期リターンすることを意味します。これは、再帰的な型定義の際に、同じ型が複数回setTypeId
に渡される可能性があるため、重複してIDを割り当てたり、無限ループに陥ったりするのを防ぐためのガードです。 -
func (s *sliceType) init(elem gobType)
内の変更:if elem.id() == 0 { setTypeId(elem) }
sliceType
の初期化(init
メソッド)において、スライスの要素型(elem
)のtypeId
がまだ割り当てられていない(elem.id() == 0
)場合に、明示的にsetTypeId(elem)
を呼び出してIDを割り当てます。これが、相互再帰的なスライスが正しく処理されるための主要な修正点の一つです。スライスが自身の要素型を定義する際に、その要素型がまだ完全に構築されていない(IDがゼロ)状態であっても、ここで強制的にIDを割り当てることで、型情報の不整合を防ぎます。 -
func newTypeObject(...)
内のcase reflect.Struct:
ブロックの変更:if gt.id() == 0 { setTypeId(gt) }
構造体型(
reflect.Struct
)を処理するnewTypeObject
関数内で、構造体のフィールドの型(gt
)がまだtypeId
を持っていない(gt.id() == 0
)場合に、setTypeId(gt)
を呼び出してIDを割り当てます。これは、相互再帰的な構造体の場合に、ある構造体のフィールドが、まだIDが割り当てられていない別の構造体を参照する状況に対応するための修正です。スライスの場合と同様に、不完全な型情報が伝播するのを防ぎ、gob
が正しく型を識別できるようにします。
これらの変更は、gob
が再帰的な型、特に相互に参照し合う構造体やスライスを処理する際に、型IDの割り当てが適切に行われるようにするためのものです。これにより、エンコード/デコードのプロセス中に型情報が欠落したり、不整合が生じたりする問題が解決されました。
関連リンク
- Go Issue 2995: https://code.google.com/p/go/issues/detail?id=2995 (古いGoogle Codeのリンクですが、これが参照されているIssueです)
- Go CL 5675083: https://golang.org/cl/5675083 (Goの変更リストへのリンク)
参考にした情報源リンク
- Go Issue 2995の議論内容
- Go
encoding/gob
パッケージのドキュメント - Go
reflect
パッケージのドキュメント - Go言語のポインタとスライスに関する一般的な情報
- バイナリ互換性に関する一般的なソフトウェア工学の概念