[インデックス 15035] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、空のJSON配列 [] を []interface{} 型にアンマーシャルする際の挙動のバグを修正します。具体的には、空のJSON配列が []interface{}(nil) (nilスライス) ではなく、[]interface{}{} (空だがnilではないスライス) として正しくアンマーシャルされるように変更されました。これにより、JSONのセマンティクスとGoの型の整合性が向上しました。
コミット
commit e57939590518b3af48dcddee0394339ef9ede1cc
Author: Andrey Mirtchovski <mirtchovski@gmail.com>
Date: Wed Jan 30 09:10:32 2013 -0800
encoding/json: properly unmarshal empty arrays.
The JSON unmarshaller failed to allocate an array when there
are no values for the input causing the `[]` unmarshalled
to []interface{} to generate []interface{}(nil) rather than
[]interface{}{}. This wasn't caught in the tests because Decode()
works correctly and because jsonBig never generated zero-sized
arrays. The modification to scanner_test.go quickly triggers
the error:
without the change to decoder.go, but with the change to scanner_test.go:
$ go test
--- FAIL: TestUnmarshalMarshal (0.10 seconds)
decode_test.go:446: Marshal jsonBig
scanner_test.go:206: diverge at 70: «03c1OL6$\":null},{\"[=» vs «03c1OL6$\":[]},{\"[=^\\»
FAIL
exit status 1
FAIL encoding/json 0.266s
Also added a simple regression to decode_test.go.
R=adg, dave, rsc
CC=golang-dev
https://golang.org/cl/7196050
---
src/pkg/encoding/json/decode.go | 2 +-\n src/pkg/encoding/json/decode_test.go | 6 ++++++\n src/pkg/encoding/json/scanner_test.go | 3 ---\n 3 files changed, 7 insertions(+), 4 deletions(-)\n
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e57939590518b3af48dcddee0394339ef9ede1cc
元コミット内容
このコミットは、encoding/json パッケージが空のJSON配列 [] をGoの []interface{} 型にアンマーシャルする際に、誤って []interface{}(nil) (nilスライス) を生成してしまう問題を修正するものです。本来は []interface{}{} (空だがnilではないスライス) が期待されていました。
この問題は、既存のテストスイートでは捕捉されていませんでした。その理由は、json.Decode() 関数がこのケースで正しく動作していたこと、そしてテスト用の大きなJSONデータ生成器である jsonBig がゼロサイズの配列を生成していなかったためです。
コミットメッセージには、scanner_test.go に一時的な変更を加えることでこのバグが再現可能になったことが示されています。その変更を加えた状態で go test を実行すると、TestUnmarshalMarshal が失敗し、scanner_test.go の特定の行で期待される出力と実際の出力が異なることが示されました。
この修正には、decoder.go の変更と、このバグを再発させないための decode_test.go へのシンプルな回帰テストの追加が含まれています。
変更の背景
Go言語の encoding/json パッケージは、JSONデータをGoのデータ構造に変換(アンマーシャル)する際に広く利用されます。このコミットが修正しようとした問題は、JSONの空配列 [] がGoの []interface{} 型にアンマーシャルされる際の、Goのスライスの内部的な挙動に関するものです。
Goにおいて、スライスは3つの要素(ポインタ、長さ、容量)を持つ構造体です。
var s []Tのように宣言されたスライスは「nilスライス」と呼ばれ、ポインタはnil、長さと容量は0です。s := make([]T, 0)のように作成されたスライスは「空スライス」と呼ばれ、ポインタはnilではありませんが、長さと容量は0です。
多くのGoのコードでは、nilスライスと空スライスは同じように扱われます(例えば、len(s) はどちらも0を返します)。しかし、JSONのアンマーシャルにおいては、この違いがセマンティックな意味を持つことがあります。JSONにおいて null は「値がない」ことを意味し、空配列 [] は「要素が0個の配列」を意味します。Goの encoding/json パッケージは、JSONの null をGoのnil値(例えばnilスライス)にマッピングするのが一般的です。したがって、JSONの空配列 [] は、Goの空だがnilではないスライス []interface{}{} にマッピングされるべきだと考えられます。
しかし、このバグが存在するバージョンでは、[] が []interface{}(nil) にアンマーシャルされていました。これは、JSONの null と空配列 [] の区別がGoの型システム上で曖昧になる可能性があり、特に後続の処理でスライスのnilチェックを行う場合に予期せぬ挙動を引き起こす可能性がありました。例えば、if mySlice == nil のようなチェックは、空配列がアンマーシャルされた場合でもtrueを返してしまうことになります。
この問題が既存のテストで捕捉されなかったのは、テストデータがゼロサイズの配列を十分にカバーしていなかったためです。また、json.Decode() はストリームから読み込むため、内部的なバッファリングや処理の都合上、この特定の問題を回避できていた可能性があります。
前提知識の解説
Goにおけるnilスライスと空スライス
Go言語のスライスは、配列への参照、長さ、容量を持つデータ構造です。
nilスライス:var s []intのように宣言されたスライスはnilスライスです。その内部ポインタはnilであり、長さ (len(s)) と容量 (cap(s)) はどちらも0です。nilスライスは、スライスがまだ初期化されていない状態や、存在しないことを表すためによく使われます。- 空スライス:
s := []int{}またはs := make([]int, 0)のように作成されたスライスは空スライスです。その内部ポインタはnilではありませんが、長さ (len(s)) と容量 (cap(s)) はどちらも0です。空スライスは、スライスが初期化されており、現在要素を持っていない状態を表します。
多くのGoの関数や操作では、nil スライスと空スライスは同じように扱われます(例: for range ループはどちらも実行されない、len() はどちらも0を返す)。しかし、json.Unmarshal のような特定のコンテキストでは、この違いが重要になることがあります。特に、JSONの null と空配列 [] のセマンティクスをGoの型に正確にマッピングする際には、この区別が不可欠です。
JSONのnullと空配列[]のセマンティクス
JSON (JavaScript Object Notation) は、データ交換のための軽量なデータ形式です。JSONにはいくつかの基本的なデータ型があります。
null: 値が存在しないことを明示的に示します。- 空配列
[]: 要素を一つも含まない配列を表します。これは「配列が存在しない」のではなく、「要素が0個の配列が存在する」ことを意味します。
これらのセマンティクスの違いは、Goの型にアンマーシャルする際に重要です。一般的に、JSONの null はGoの型のゼロ値(ポインタ型やスライス型の場合は nil)にマッピングされ、JSONの空配列 [] はGoの空スライス(make([]T, 0))にマッピングされるべきです。
Goのencoding/jsonパッケージ
encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。
json.Marshal(): Goのデータ構造をJSONバイトスライスにエンコードします。json.Unmarshal(): JSONバイトスライスをGoのデータ構造にデコードします。json.Encoder/json.Decoder: ストリームベースでJSONのエンコード/デコードを行います。
このパッケージは、Goの構造体のフィールドタグ(例: json:"fieldName")を使用して、JSONフィールドとGoの構造体フィールドのマッピングを制御します。また、json.Marshaler および json.Unmarshaler インターフェースを実装することで、カスタムのJSONエンコード/デコードロジックを提供できます。
interface{}
Goの interface{} (空インターフェース) は、任意の型の値を保持できる特別なインターフェースです。これは、型が事前に不明な場合や、異なる型の値を統一的に扱いたい場合に非常に便利です。encoding/json パッケージは、JSONデータをGoの具体的な型にアンマーシャルする際に、ターゲットの型が interface{} である場合、JSONの型に基づいて適切なGoの型(例: JSONオブジェクトは map[string]interface{}、JSON配列は []interface{}、JSON文字列は string など)を動的に割り当てます。
技術的詳細
このコミットの核心は、encoding/json パッケージがJSONの空配列 [] を []interface{} 型にアンマーシャルする際の、Goのスライスの初期化方法の変更にあります。
encoding/json パッケージの内部では、JSON配列をGoのスライスに変換する際に arrayInterface() という関数が使用されます。この関数は、[]interface{} 型のスライスを構築する責任を負っています。
修正前の arrayInterface() 関数では、スライス v が var v []interface{} として宣言されていました。この宣言は、Goにおいて v を nil スライスとして初期化します。その後、JSON配列の要素が読み込まれると、スライスに要素が追加されていきます。しかし、入力されたJSON配列が空 ([]) であった場合、ループ内で要素が追加されることはなく、結果として v は初期化時の nil スライスのまま返されていました。
これは、JSONの null がGoの nil にマッピングされるという一般的な慣習と衝突し、JSONの空配列 [] と null の区別がGoの型システム上で失われる原因となっていました。
修正では、arrayInterface() 関数内のスライス v の初期化が var v = make([]interface{}, 0) に変更されました。
make([]interface{}, 0)は、長さと容量が0の「空だがnilではない」スライスを作成します。- この変更により、入力されたJSON配列が空であっても、
arrayInterface()はnilスライスではなく、正しく空だがnilではないスライス[]interface{}{}を返すようになります。
この修正は、JSONのセマンティクス(null と [] の違い)をGoの型システムに正確に反映させるために重要です。これにより、開発者はJSONの空配列がアンマーシャルされた際に、それが nil ではないことを安全に仮定できるようになり、より堅牢なコードを書くことができます。
scanner_test.go の変更(削除された3行)は、このバグを再現させるための一時的な変更でした。genArray 関数はテスト用の配列を生成するもので、削除されたコードは n > 0 && f == 0 の場合に f を 1 に設定していました。これは、genArray がゼロサイズの配列を生成しないようにするものでした。この行を削除することで、genArray がゼロサイズの配列を生成できるようになり、それによって encoding/json のアンマーシャルにおけるバグが露呈しました。バグが修正されたため、このテスト用の変更は元に戻されました。
decode_test.go に追加された回帰テストは、この修正が正しく機能していることを確認し、将来的に同じバグが再発しないようにするためのものです。特に、{in: [], ptr: new([]interface{}), out: []interface{}{}} のテストケースは、空のJSON配列が []interface{} にアンマーシャルされた際に、期待通り空だがnilではないスライスが生成されることを明示的に検証しています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go の arrayInterface() 関数内のスライス初期化の変更がコアとなります。
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -742,7 +742,7 @@ func (d *decodeState) valueInterface() interface{} {
// arrayInterface is like array but returns []interface{}.
func (d *decodeState) arrayInterface() []interface{} {
- var v []interface{}
+ var v = make([]interface{}, 0)
for {
// Look ahead for ] - can only happen on first iteration.
op := d.scanWhile(scanSkipSpace)
また、src/pkg/encoding/json/decode_test.go には、この修正を検証するための新しいテストケースが追加されました。
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -239,6 +239,12 @@ var unmarshalTests = []unmarshalTest{\n {in: `[1, 2, 3]`, ptr: new([1]int), out: [1]int{1}},\n {in: `[1, 2, 3]`, ptr: new([5]int), out: [5]int{1, 2, 3, 0, 0}},\n
+\t// empty array to interface test\n+\t{in: `[]`, ptr: new([]interface{}), out: []interface{}{}},\n+\t{in: `null`, ptr: new([]interface{}), out: []interface{}(nil)},\n+\t{in: `{\"T\":[]}`, ptr: new(map[string]interface{}), out: map[string]interface{}{\"T\": []interface{}{}}},\n+\t{in: `{\"T\":null}`, ptr: new(map[string]interface{}), out: map[string]interface{}{\"T\": interface{}(nil)}},\n+\n \t// composite tests\n \t{in: allValueIndent, ptr: new(All), out: allValue},\n \t{in: allValueCompact, ptr: new(All), out: allValue},\
そして、バグを再現させるために一時的に追加されていた src/pkg/encoding/json/scanner_test.go の変更が元に戻されました。
--- a/src/pkg/encoding/json/scanner_test.go
+++ b/src/pkg/encoding/json/scanner_test.go
@@ -277,9 +277,6 @@ func genArray(n int) []interface{} {\n if f > n {\n f = n\n }\n-\tif n > 0 && f == 0 {\n-\t\tf = 1\n-\t}\n \tx := make([]interface{}, f)\n \tfor i := range x {\n \t\tx[i] = genValue(((i+1)*n)/f - (i*n)/f)\
コアとなるコードの解説
src/pkg/encoding/json/decode.go の変更は、arrayInterface() 関数内の以下の1行です。
- var v []interface{}
+ var v = make([]interface{}, 0)
-
変更前 (
var v []interface{}): この行は、vを[]interface{}型のnilスライスとして宣言し、初期化します。Goでは、スライスをこのように宣言すると、そのポインタはnilになり、長さと容量は0になります。arrayInterface()関数がJSON配列の要素を読み込む際に、もし配列が空であれば、このvには何も追加されず、結果としてnilスライスが返されていました。これは、JSONの空配列[]がGoのnilスライスにマッピングされるという、意図しない挙動を引き起こしていました。 -
変更後 (
var v = make([]interface{}, 0)): この行は、vを長さと容量が0の「空だがnilではない」スライスとして初期化します。make関数は、スライスの基盤となる配列を割り当て、そのスライスヘッダ(ポインタ、長さ、容量)を設定します。この場合、長さ0のスライスが作成されるため、ポインタはnilではありませんが、要素は含まれていません。 この変更により、arrayInterface()関数は、JSONの空配列[]をアンマーシャルする際に、nilスライスではなく、正しく空だがnilではないスライス[]interface{}{}を返すようになります。これにより、JSONのnullと空配列[]のセマンティクスがGoの型システムに正確に反映され、より予測可能で堅牢なJSONアンマーシャル処理が実現されます。
decode_test.go に追加されたテストケースは、この修正の正しさを保証します。特に、{in: [], ptr: new([]interface{}), out: []interface{}{}} は、空のJSON配列が []interface{} にアンマーシャルされたときに、期待される []interface{}{} (空だがnilではないスライス) が得られることを明示的に確認しています。
scanner_test.go の変更は、バグを再現させるためのテストコードの調整であり、本質的な修正ではありません。バグが修正されたため、そのテストコードは元の状態に戻されました。
関連リンク
- Go Code Review:
https://golang.org/cl/7196050
参考にした情報源リンク
- Go Slices: usage and internals: https://go.dev/blog/slices
- The Go Programming Language Specification - Slice types: https://go.dev/ref/spec#Slice_types
- encoding/json package documentation: https://pkg.go.dev/encoding/json
- JSON (JavaScript Object Notation) - ECMA-404: https://www.ecma-international.org/publications-and-standards/standards/ecma-404/
- RFC 7159 - The JavaScript Object Notation (JSON) Data Interchange Format: https://datatracker.ietf.org/doc/html/rfc7159
- Go: nil slice vs empty slice: https://yourbasic.org/golang/nil-slice-empty-slice/ (一般的なGoのブログ記事やチュートリアルでこのトピックはよく扱われます)