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

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

このコミットは、Go言語のmath/bigパッケージにおけるgobエンコーディング/デコーディング時のnilポインタのバグを修正するものです。具体的には、*Int型および*Rat型のスライス内にnilポインタが存在する場合に、gobが正しく処理できない問題に対処しています。

コミット

commit bc6bb3efb4f2f75375ab8820ee536f696269c6b4
Author: Rob Pike <r@golang.org>
Date:   Mon Aug 19 11:22:09 2013 +1000

    math/big: fix nil bug in GobEncode
    
    Update #5305.
    This handles the case where the nil pointers are inside a slice.
    A top-level nil pointer is harder, maybe fundamentally broken by gob's model.
    Thinking required.
    However, a slice is the important case since people don't expect to be sending
    top-level nils much, but they can arise easily in slices.
    
    R=golang-dev, josharian, adg
    CC=golang-dev
    https://golang.org/cl/13042044

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

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

元コミット内容

math/bigパッケージにおけるGobEncodeの実装でnilバグを修正します。 これは、nilポインタがスライス内に存在する場合を扱います。 トップレベルのnilポインタはより困難であり、おそらくgobのモデルによって根本的に壊れている可能性があります。検討が必要です。 しかし、スライスは重要なケースです。なぜなら、人々はトップレベルのnilをあまり送信することを期待していませんが、スライス内では容易に発生しうるからです。 Go issue #5305の更新です。

変更の背景

このコミットは、Go言語のgobパッケージを使用してmath/bigパッケージの型(*Int*Rat)をエンコードおよびデコードする際に発生する特定のバグに対処するために行われました。具体的には、*Int*Ratのポインタを含むスライスがgobでエンコードされ、そのスライス内にnilポインタが含まれている場合に、デコード時に問題が発生するというバグです。

コミットメッセージでは「Update #5305」と記載されており、これはGoのIssueトラッカーにおける特定のバグ報告に対応する修正であることを示唆しています。ただし、現在の検索では直接的なIssue #5305の詳細は見つかりませんでした。しかし、コミットメッセージから、この問題がgobのnilポインタの扱い、特にスライス内のnilポインタの扱いに関連していることが明確です。

gobはGoのデータ構造をシリアライズ/デシリアライズするためのメカニズムであり、ネットワーク経由でのデータ転送や永続化によく使用されます。gobは、エンコードされるデータの型情報も一緒に送信するため、デコード時に正しい型に復元できます。しかし、ポインタ型、特にnilポインタの扱いは、シリアライズシステムにとって複雑な課題となることがあります。このバグは、math/bigパッケージの数値型がgobで適切に扱われることを保証するために修正されました。

前提知識の解説

  • math/bigパッケージ: Go言語の標準ライブラリの一部で、任意精度の整数(Int)、有理数(Rat)、浮動小数点数(Float)を扱うためのパッケージです。金融計算や暗号学など、高い精度が要求される場面で利用されます。これらの型は通常、ポインタ(例: *Int)として扱われることが多いです。
  • encoding/gobパッケージ: Go言語の標準ライブラリの一部で、Goのデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのパッケージです。gobは、エンコードされるデータの型情報を自己記述的に含めるため、受信側は事前に型を知らなくてもデータをデコードできます。これは、RPC(Remote Procedure Call)や永続化のシナリオで非常に便利です。
  • GobEncoderおよびGobDecoderインターフェース: gobパッケージは、カスタムのエンコード/デコードロジックを定義するためのインターフェースを提供します。
    • GobEncode() ([]byte, error): 型がgobでエンコードされる際に呼び出されます。このメソッドは、型をバイトスライスに変換する責任を持ちます。
    • GobDecode([]byte) error: 型がgobでデコードされる際に呼び出されます。このメソッドは、バイトスライスから型を復元する責任を持ちます。 これらのインターフェースを実装することで、開発者はgobのデフォルトの動作をオーバーライドし、特定の型をどのようにシリアライズ/デシリアライズするかを細かく制御できます。
  • Goにおけるnilポインタ: Goでは、ポインタは変数のメモリアドレスを保持します。ポインタが何も指していない場合、それはnil値を取ります。nilポインタをデリファレンスしようとすると、ランタイムパニックが発生します。gobのようなシリアライズシステムでは、nilポインタをどのように表現し、デコード時にどのように復元するかが重要になります。特に、スライスのようなコレクション内にnilポインタが含まれる場合、その扱いはより複雑になります。

技術的詳細

このコミットの技術的な核心は、math/bigパッケージの*Int*Rat型がgobエンコーディング/デコーディング時にnilポインタを適切に処理できるようにすることです。

従来のgobの動作では、スライス内のnilポインタがエンコードされると、デコード時に問題が発生する可能性がありました。特に、GobEncodeメソッドがnilレシーバで呼び出された場合(Goではポインタレシーバを持つメソッドはnilポインタでも呼び出し可能)、そのメソッド内でnilチェックが行われていないと、パニックを引き起こすか、不正なデータがエンコードされる可能性がありました。

この修正では、以下の2つの主要な変更が導入されました。

  1. GobEncodeメソッドにおけるnilレシーバのハンドリング: *Intおよび*RatGobEncodeメソッドの冒頭に、レシーバxnilであるかどうかのチェックが追加されました。

    if x == nil {
        return nil, nil
    }
    

    これにより、nilポインタがエンコードされる際に、空のバイトスライス(nil)が返されるようになります。これは、gobnil値を表現するための標準的な方法です。この変更により、スライス内にnilポインタが含まれていても、GobEncodeが安全に実行され、nilが正しくエンコードされるようになります。

  2. GobDecodeメソッドにおける空のバッファのハンドリング: *Intおよび*RatGobDecodeメソッドにおいて、入力バッファbufの長さが0である場合の処理が変更されました。

    if len(buf) == 0 {
        // Other side sent a nil or default value.
        *z = Int{} // or Rat{}
        return nil
    }
    

    以前は、len(buf) == 0の場合にerrors.New("Int.GobDecode: no data")のようなエラーを返していました。しかし、GobEncodeがnilポインタをnilバイトスライスとしてエンコードするようになったため、デコード側ではlen(buf) == 0が「エンコードされた値がnilであった」ことを意味するようになりました。 この変更により、len(buf) == 0の場合には、デコード対象のポインタzが指す値がその型のゼロ値(Int{}Rat{})に設定されるようになりました。これにより、nilポインタがデコードされた際に、対応する型のゼロ値が適切に復元され、ランタイムエラーを防ぎます。

これらの変更により、math/bigパッケージの*Intおよび*Rat型が、スライス内でnilポインタとして存在する場合でも、gobによるシリアライズ/デシリアライズが安全かつ正確に行われるようになりました。コミットメッセージにあるように、トップレベルのnilポインタの扱いはより複雑ですが、スライス内のnilポインタの修正は、より一般的なユースケースに対応する重要な改善です。

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

src/pkg/math/big/int.go

--- a/src/pkg/math/big/int.go
+++ b/src/pkg/math/big/int.go
@@ -952,6 +952,9 @@ const intGobVersion byte = 1
 
 // GobEncode implements the gob.GobEncoder interface.
 func (x *Int) GobEncode() ([]byte, error) {
+	if x == nil {
+		return nil, nil
+	}
 	buf := make([]byte, 1+len(x.abs)*_S) // extra byte for version and sign bit
 	i := x.abs.bytes(buf) - 1            // i >= 0
 	b := intGobVersion << 1              // make space for sign bit
@@ -965,7 +968,9 @@ func (x *Int) GobEncode() ([]byte, error) {
 // GobDecode implements the gob.GobDecoder interface.
 func (z *Int) GobDecode(buf []byte) error {
 	if len(buf) == 0 {
-		return errors.New("Int.GobDecode: no data")
+		// Other side sent a nil or default value.
+		*z = Int{}
+		return nil
 	}
 	b := buf[0]
 	if b>>1 != intGobVersion {

src/pkg/math/big/int_test.go

--- a/src/pkg/math/big/int_test.go
+++ b/src/pkg/math/big/int_test.go
@@ -1484,6 +1484,32 @@ func TestIntGobEncoding(t *testing.T) {
 	}
 }
 
+// Sending a nil Int pointer (inside a slice) on a round trip through gob should yield a zero.
+// TODO: top-level nils.
+func TestGobEncodingNilIntInSlice(t *testing.T) {
+	buf := new(bytes.Buffer)
+	enc := gob.NewEncoder(buf)
+	dec := gob.NewDecoder(buf)
+
+	var in = make([]*Int, 1)
+	err := enc.Encode(&in)
+	if err != nil {
+		t.Errorf("gob encode failed: %q", err)
+	}
+	var out []*Int
+	err = dec.Decode(&out)
+	if err != nil {
+		t.Fatalf("gob decode failed: %q", err)
+	}
+	if len(out) != 1 {
+		t.Fatalf("wrong len; want 1 got %d", len(out))
+	}
+	var zero Int
+	if out[0].Cmp(&zero) != 0 {
+		t.Errorf("transmission of (*Int)(nill) failed: got %s want 0", out)
+	}
+}
+
 func TestIntJSONEncoding(t *testing.T) {
 	for _, test := range encodingTests {
 		var tx Int

src/pkg/math/big/rat.go

--- a/src/pkg/math/big/rat.go
+++ b/src/pkg/math/big/rat.go
@@ -546,6 +546,9 @@ const ratGobVersion byte = 1
 
 // GobEncode implements the gob.GobEncoder interface.
 func (x *Rat) GobEncode() ([]byte, error) {
+	if x == nil {
+		return nil, nil
+	}
 	buf := make([]byte, 1+4+(len(x.a.abs)+len(x.b.abs))*_S) // extra bytes for version and sign bit (1), and numerator length (4)
 	i := x.b.abs.bytes(buf)
 	j := x.a.abs.bytes(buf[0:i])
@@ -567,7 +570,9 @@ func (x *Rat) GobDecode(buf []byte) error {
 // GobDecode implements the gob.GobDecoder interface.
 func (z *Rat) GobDecode(buf []byte) error {
 	if len(buf) == 0 {
-		return errors.New("Rat.GobDecode: no data")
+		// Other side sent a nil or default value.
+		*z = Rat{}
+		return nil
 	}
 	b := buf[0]
 	if b>>1 != ratGobVersion {

src/pkg/math/big/rat_test.go

--- a/src/pkg/math/big/rat_test.go
+++ b/src/pkg/math/big/rat_test.go
@@ -407,6 +407,32 @@ func TestRatGobEncoding(t *testing.T) {
 	}
 }
 
+// Sending a nil Rat pointer (inside a slice) on a round trip through gob should yield a zero.
+// TODO: top-level nils.
+func TestGobEncodingNilRatInSlice(t *testing.T) {
+	buf := new(bytes.Buffer)
+	enc := gob.NewEncoder(buf)
+	dec := gob.NewDecoder(buf)
+
+	var in = make([]*Rat, 1)
+	err := enc.Encode(&in)
+	if err != nil {
+		t.Errorf("gob encode failed: %q", err)
+	}
+	var out []*Rat
+	err = dec.Decode(&out)
+	if err != nil {
+		t.Fatalf("gob decode failed: %q", err)
+	}
+	if len(out) != 1 {
+		t.Fatalf("wrong len; want 1 got %d", len(out))
+	}
+	var zero Rat
+	if out[0].Cmp(&zero) != 0 {
+		t.Errorf("transmission of (*Int)(nill) failed: got %s want 0", out)
+	}
+}
+
 func TestIssue2379(t *testing.T) {
 	// 1) no aliasing
 	q := NewRat(3, 2)

コアとなるコードの解説

GobEncodeの変更

GobEncodeメソッドは、*Intまたは*Rat型の値がgobエンコーダによってバイトストリームに変換される際に呼び出されます。追加された以下のコードブロックが重要です。

if x == nil {
    return nil, nil
}

これは、GobEncodeがnilポインタレシーバ(xnilである場合)で呼び出されたときに、nilバイトスライスとnilエラーを返すようにします。gobは、nilバイトスライスを特定の型のnil値として解釈します。これにより、スライス内にnilポインタが含まれている場合でも、GobEncodeが安全に実行され、nilが正しくエンコードされるようになります。以前は、xnilの場合に、その後のx.absなどのアクセスでパニックが発生する可能性がありました。

GobDecodeの変更

GobDecodeメソッドは、gobデコーダによってバイトストリームから*Intまたは*Rat型の値が復元される際に呼び出されます。変更された以下のコードブロックが重要です。

if len(buf) == 0 {
    // Other side sent a nil or default value.
    *z = Int{} // or Rat{}
    return nil
}

以前のバージョンでは、bufが空の場合(つまり、エンコードされたデータがない場合)にエラーを返していました。しかし、GobEncodeの変更により、nilポインタが空のバイトスライスとしてエンコードされるようになったため、デコード側ではlen(buf) == 0が「エンコードされた値がnilであった」ことを意味するようになりました。

この修正では、len(buf) == 0の場合に、デコード対象のポインタzが指す値がその型のゼロ値(Int{}またはRat{})に設定されるようになりました。*z = Int{}は、zが指すInt構造体のすべてのフィールドをそのゼロ値に初期化します。これにより、nilポインタがデコードされた際に、対応する型のゼロ値が適切に復元され、ランタイムエラーを防ぎ、期待される動作(nilがゼロ値として復元される)を実現します。

テストケースの追加

int_test.gorat_test.goには、TestGobEncodingNilIntInSliceTestGobEncodingNilRatInSliceという新しいテストケースが追加されています。これらのテストは、*Intまたは*Ratのnilポインタを含むスライスをgobでエンコードし、その後デコードして、結果が期待通りにゼロ値として復元されることを確認します。これにより、このコミットで修正されたバグが将来的に再発しないように保証されます。

関連リンク

参考にした情報源リンク