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

[インデックス 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パッケージが、相互に再帰する構造体のスライス(例: []*MyStructMyStruct が自身へのポインタを持つ場合)を正しくエンコード/デコードできないというバグ(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のあらゆる型の実行時表現を提供し、型の名前、フィールド、メソッドなどの情報を取得できます。gobreflectパッケージを利用して、エンコード/デコード対象の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の議論内容
  • Go encoding/gobパッケージのドキュメント
  • Go reflectパッケージのドキュメント
  • Go言語のポインタとスライスに関する一般的な情報
  • バイナリ互換性に関する一般的なソフトウェア工学の概念