[インデックス 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つの主要な変更が導入されました。
-
GobEncode
メソッドにおけるnilレシーバのハンドリング:*Int
および*Rat
のGobEncode
メソッドの冒頭に、レシーバx
がnil
であるかどうかのチェックが追加されました。if x == nil { return nil, nil }
これにより、
nil
ポインタがエンコードされる際に、空のバイトスライス(nil
)が返されるようになります。これは、gob
がnil
値を表現するための標準的な方法です。この変更により、スライス内にnil
ポインタが含まれていても、GobEncode
が安全に実行され、nil
が正しくエンコードされるようになります。 -
GobDecode
メソッドにおける空のバッファのハンドリング:*Int
および*Rat
のGobDecode
メソッドにおいて、入力バッファ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ポインタレシーバ(x
がnil
である場合)で呼び出されたときに、nil
バイトスライスとnil
エラーを返すようにします。gob
は、nil
バイトスライスを特定の型のnil
値として解釈します。これにより、スライス内にnil
ポインタが含まれている場合でも、GobEncode
が安全に実行され、nil
が正しくエンコードされるようになります。以前は、x
がnil
の場合に、その後の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.go
とrat_test.go
には、TestGobEncodingNilIntInSlice
とTestGobEncodingNilRatInSlice
という新しいテストケースが追加されています。これらのテストは、*Int
または*Rat
のnilポインタを含むスライスをgob
でエンコードし、その後デコードして、結果が期待通りにゼロ値として復元されることを確認します。これにより、このコミットで修正されたバグが将来的に再発しないように保証されます。
関連リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
math/big
パッケージのドキュメント: https://pkg.go.dev/math/bigencoding/gob
パッケージのドキュメント: https://pkg.go.dev/encoding/gob- GoのIssueトラッカー: https://github.com/golang/go/issues
参考にした情報源リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/bc6bb3efb4f2f75375ab8820ee536f696269c6b4
- Go CL 13042044: https://golang.org/cl/13042044 (これはコミットメッセージに記載されているGoのコードレビューシステムへのリンクです)
- Go言語のnilポインタに関する一般的な情報源 (例: Effective Goなど)