[インデックス 10389] ファイルの概要
コミット
コミットハッシュ: 53523f6a7d6ca6aa6ec4a1d20af773f333284371
作成者: Russ Cox rsc@golang.org
日付: Mon Nov 14 16:03:23 2011 -0500
タイトル: encoding/json: decode [] as empty slice, not nil slice
変更ファイル: src/pkg/encoding/json/decode.go (4行追加)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/53523f6a7d6ca6aa6ec4a1d20af773f333284371
元コミット内容
encoding/json: decode [] as empty slice, not nil slice
Test was already present, but bug in reflect.DeepEqual hid this bug.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/5375090
変更内容:
src/pkg/encoding/json/decode.go
のarray
関数に3行のコードを追加- 空のJSONアレイ
[]
をデコードする際に、nil sliceではなく空のsliceを作成するよう修正
変更の背景
このコミットは、Go言語のJSON パッケージにおける重要な一貫性の問題を修正したものです。2011年の時点で、Goのencoding/json
パッケージは以下のような矛盾した振る舞いを持っていました:
- エンコード時: nil sliceは
null
として、空のslice([]string{}
)は[]
としてエンコードされる - デコード時: 空のJSONアレイ
[]
がnil sliceとしてデコードされる
この非対称性は、JSONのラウンドトリップ(エンコード→デコード)において予期しない結果を生み出していました。例えば:
// エンコード前
data := []string{} // 空のslice(非nil)
// JSONエンコード
json := `[]`
// デコード後
var result []string // nil slice
この問題は、特にAPI設計において重要な意味を持ちます。多くのAPIでは、null
と[]
は異なる意味を持つため、この一貫性の欠如は深刻な問題となっていました。
前提知識の解説
Goにおけるnil sliceと空のsliceの違い
Goでは、sliceには以下の2つの状態があります:
-
nil slice:
var s []string
またはs := []string(nil)
- 内部的にはポインタがnilを指している
len(s) == 0
、cap(s) == 0
、s == nil
はtrue
- メモリ効率が良い(メモリ割り当てが不要)
-
空のslice:
s := []string{}
またはs := make([]string, 0)
- 内部的には有効なポインタを持つが、要素数が0
len(s) == 0
、cap(s) == 0
、s == nil
はfalse
- わずかなメモリ使用量がある
JSONとsliceの関係
JSONにおいて:
null
は値の不在を表す[]
は空のアレイを表す
これらは意味的に異なるものですが、Goのnil sliceと空のsliceの区別と自然に対応します。
reflect.DeepEqualの問題
コミットメッセージで言及されている「bug in reflect.DeepEqual hid this bug」について:
reflect.DeepEqual
は、nil sliceと空のsliceを異なるものとして扱います:
var nilSlice []string
emptySlice := []string{}
fmt.Println(reflect.DeepEqual(nilSlice, emptySlice)) // false
この動作により、既存のテストでは空のJSONアレイをデコードした結果がnil sliceになっていても、テストが適切に失敗していませんでした。
技術的詳細
修正前の動作
修正前のコードでは、空のJSONアレイ[]
をデコードすると、以下のような流れで処理されていました:
array
関数が呼び出される- 配列要素の解析ループが実行される(要素数は0)
- ループ変数
i
は0のまま - slice lengthの調整処理は実行されるが、nil sliceの場合は何も行われない
- 結果として、デコード先のsliceはnilのまま残る
修正後の動作
修正後は、以下の処理が追加されました:
if i == 0 && av.Kind() == reflect.Slice && sv.IsNil() {
sv.Set(reflect.MakeSlice(sv.Type(), 0, 0))
}
この条件は以下をチェックします:
i == 0
: 配列要素が0個だったav.Kind() == reflect.Slice
: 対象がslice型であるsv.IsNil()
: 現在のsliceがnilである
これらの条件が満たされた場合、reflect.MakeSlice(sv.Type(), 0, 0)
を使って長さ0、容量0の空のsliceを作成し、設定します。
リフレクションの詳細
この修正は、Goのリフレクション機能を使用して実装されています:
reflect.Value.Kind()
: 値の種類(slice、array等)を取得reflect.Value.IsNil()
: 値がnilかどうかを判定reflect.MakeSlice(typ, len, cap)
: 指定された型、長さ、容量のsliceを作成reflect.Value.Set()
: 値を設定
コアとなるコードの変更箇所
ファイル: src/pkg/encoding/json/decode.go
関数: array
関数
行数: 32-34行目(追加された部分)
@@ -392,6 +393,9 @@ func (d *decodeState) array(v reflect.Value) {
sv.SetLen(i)
}
}
+ if i == 0 && av.Kind() == reflect.Slice && sv.IsNil() {
+ sv.Set(reflect.MakeSlice(sv.Type(), 0, 0))
+ }
}
コアとなるコードの解説
修正されたarray関数の構造
array
関数は、JSONアレイをGoのarray/sliceにデコードする処理を行います:
- 初期化フェーズ: 対象の型を確認し、array/sliceに応じた処理を準備
- 要素解析ループ: JSON内の各要素を順次デコード
- 長さ調整フェーズ: 必要に応じてsliceの長さを調整
- 空配列特別処理 (今回の修正): 空の配列の場合の特別な処理
追加されたコードの詳細解説
if i == 0 && av.Kind() == reflect.Slice && sv.IsNil() {
sv.Set(reflect.MakeSlice(sv.Type(), 0, 0))
}
各条件の意味:
-
i == 0
:- 変数
i
は解析された配列要素の数を示す - 0の場合、空のJSONアレイ
[]
がデコードされたことを意味
- 変数
-
av.Kind() == reflect.Slice
:av
はreflect.Value
型の変数で、デコード対象の型情報を持つ- この条件でslice型(array型ではない)であることを確認
-
sv.IsNil()
:sv
は実際の値を保持するreflect.Value
- nilである場合、まだ実際のsliceインスタンスが作成されていない
実行される処理:
-
reflect.MakeSlice(sv.Type(), 0, 0)
:- 第1引数: sliceの型(元の型を保持)
- 第2引数: 長さ(0)
- 第3引数: 容量(0)
- 戻り値: 新しい空のsliceの
reflect.Value
-
sv.Set(...)
: 作成した空のsliceを実際の変数に設定
パフォーマンスへの影響
この修正は最小限のパフォーマンス影響で実装されています:
- 条件チェック: 3つの単純な条件チェックのみ
- slice作成: 空のsliceの作成は軽量な操作
- 実行頻度: 空のJSONアレイのデコード時のみ実行
関連リンク
- Go公式ドキュメント - encoding/json
- Go公式ドキュメント - reflect
- Go Wiki - CodeReview
- Go言語仕様 - Slice types
- Go Blog - JSON and Go
参考にした情報源リンク
- GitHub Issue #37711 - proposal: encoding/json: nilasempty to encode nil-slices as []
- Medium - Handling Null JSON Arrays in Go
- GitHub Issue #26866 - json unmarshal "null" to empty slice variable
- Stack Overflow - Return an empty array instead of null with golang for json
- YourBasic Go - How to best clear a slice: empty vs. nil
- Go Package Documentation - encoding/json
- Go Source Code - decode.go