[インデックス 10874] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json
パッケージにおける、JSON配列のデコード処理に関するバグ修正とコードのクリーンアップを目的としています。具体的には、reflect
パッケージのAPI変更によって残された不要な変数を除去し、配列とスライスの型チェックおよび動的なリサイズ処理の不具合を修正しています。これにより、JSONからGoの配列やスライスへのデコードがより堅牢かつ正確に行われるようになります。
コミット
commit 4a4c39e7d4f95ffcaa6971c35c4adeb740dcc515
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Mon Dec 19 15:32:06 2011 -0500
encoding/json: cleanup leftover variables in array decoding.
An old update for API changes in reflect package left several
helper variables that do not have a meaning anymore, and
the type checking of arrays vs slices was broken.
Fixes #2513.
R=ultrotter, rsc
CC=golang-dev, remy
https://golang.org/cl/5488094
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4a4c39e7d4f95ffcaa6971c35c4adeb740dcc515
元コミット内容
このコミットは、encoding/json
パッケージの配列デコードロジックにおける、残存する不要な変数のクリーンアップと、配列とスライスの型チェックの不具合修正を目的としています。コミットメッセージによると、以前の reflect
パッケージのAPI変更に伴う更新が原因で、意味を持たなくなったヘルパー変数がコード内に残されており、その結果、配列とスライスの型チェックが正しく機能していなかったとのことです。この問題は Issue #2513 として報告されており、このコミットによって修正されました。
変更の背景
Go言語の reflect
パッケージは、プログラムの実行時に型情報を検査・操作するための強力な機能を提供します。encoding/json
パッケージは、JSONデータをGoの構造体やプリミティブ型にデコードする際に、この reflect
パッケージを内部的に利用して、動的な型変換やフィールドへの値の割り当てを行っています。
コミットメッセージにある「An old update for API changes in reflect package」とは、Go言語の進化に伴う reflect
パッケージのAPI変更を指しています。Goは初期の段階で活発な開発が行われており、APIの変更は珍しいことではありませんでした。このようなAPI変更があった際、encoding/json
パッケージのコードが完全に追従しきれず、古いAPIの利用を前提とした変数やロジックが残ってしまったと考えられます。
具体的には、JSONの配列をGoの配列(固定長)やスライス(可変長)にデコードする際、reflect
パッケージを使ってターゲットの型を判別し、スライスであれば必要に応じてメモリを再割り当てして拡張する処理が行われます。このコミット以前のコードでは、古い reflect
APIの残骸が原因で、この型判別やスライス拡張のロジックに不具合が生じていたと推測されます。Issue #2513 は、おそらくこの不具合によって特定のJSON配列が正しくデコードされない、あるいは予期せぬエラーが発生するといった具体的な問題を示していたでしょう。
このコミットは、これらの残存する問題を取り除き、encoding/json
パッケージの堅牢性と正確性を向上させるために行われました。
前提知識の解説
Go言語の encoding/json
パッケージ
encoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。主に json.Marshal
(Goの値をJSONにエンコード) と json.Unmarshal
(JSONデータをGoの値にデコード) の2つの関数が中心となります。このコミットは json.Unmarshal
の内部実装、特に配列のデコード部分に関わっています。
Go言語の reflect
パッケージ
reflect
パッケージは、Goのプログラムが自身の構造を検査・操作できるようにする機能を提供します。これにより、実行時に変数の型、値、構造体のフィールドなどを動的に調べたり、変更したりすることが可能になります。
reflect.Value
: Goの変数の値を表す型です。reflect.ValueOf(x)
で任意のGoの値x
からreflect.Value
を取得できます。reflect.Type
: Goの変数の型情報を表す型です。reflect.TypeOf(x)
で任意のGoの値x
からreflect.Type
を取得できます。Kind()
:reflect.Value
またはreflect.Type
のメソッドで、その値または型がプリミティブ型(Int
,String
など)、構造体(Struct
)、配列(Array
)、スライス(Slice
)、インターフェース(Interface
)など、どのカテゴリに属するかを返します。reflect.Array
とreflect.Slice
:Kind()
メソッドが返す定数で、それぞれGoの配列とスライスを表します。Cap()
(Capacity): スライスが現在保持できる要素の最大数を返します。Len()
(Length): スライスが現在保持している要素の数を返します。MakeSlice(typ Type, len, cap int) Value
: 指定された型、長さ、容量を持つ新しいスライスを作成します。Copy(dst, src Value) int
:src
スライスの要素をdst
スライスにコピーします。SetLen(n int)
: スライスの長さをn
に設定します。
encoding/json
パッケージは、JSONの配列をGoの配列やスライスにデコードする際に、これらの reflect
パッケージの機能を利用して、ターゲットの型が配列なのかスライスなのかを判別し、スライスであれば必要に応じてその長さを調整したり、容量を増やしたりします。
Go言語の配列 (Array) とスライス (Slice)
Go言語には、固定長の「配列 (Array)」と可変長の「スライス (Slice)」という2種類のシーケンス型があります。
- 配列:
[N]Type
の形式で宣言され、要素数が固定です。例えば[3]int
は3つの整数を格納できる配列です。 - スライス:
[]Type
の形式で宣言され、配列を基盤としていますが、長さが可変です。スライスは、基盤となる配列の一部を参照するビューのようなものです。JSONの配列をGoのデータ構造にデコードする場合、通常はスライスが使われます。なぜなら、JSONの配列の長さは事前に分からないことが多く、動的に要素を追加できるスライスの方が柔軟だからです。
UnmarshalTypeError
encoding/json
パッケージがJSONデータをGoの型にデコードしようとした際に、JSONのデータ型とGoのターゲットの型が一致しない場合に発生するエラーです。例えば、JSONで数値が来ているのにGoのターゲットが文字列型だった場合などに発生します。
技術的詳細
このコミットの技術的詳細は、主に src/pkg/encoding/json/decode.go
ファイル内の decodeState
構造体の value
メソッドと array
メソッドの変更に集約されます。
-
value
メソッド内のパニック修正: 以前のコードでは、d.scan.redo
がtrue
の場合にpanic("redo")
が発生していました。これは、スキャン状態の巻き戻し処理において、予期せぬ状態に陥った際に発生するデバッグ用のパニック、あるいは未実装のロジックだった可能性があります。このコミットでは、このパニックを回避し、d.scan.redo
をfalse
にリセットし、スキャンステップをstateBeginValue
に戻すことで、正常な状態遷移を促しています。これにより、特定のJSON入力でデコードがクラッシュする問題を修正したと考えられます。 -
array
メソッド内の型チェックと変数クリーンアップ:array
メソッドはJSONの配列をGoの配列またはスライスにデコードする主要なロジックを含んでいます。-
不要な変数の削除: 以前のコードでは
iv
,ok
,av
,sv
といった複数のreflect.Value
型のヘルパー変数が使われていました。これらはreflect
パッケージのAPI変更によって冗長になったか、あるいは誤ったロジックを招いていた可能性があります。このコミットでは、これらの変数を削除し、デコード対象のv reflect.Value
を直接操作するように変更されています。これにより、コードが簡素化され、意図しない副作用が排除されました。 -
switch v.Kind()
による明確な型判別: 以前はif av.Kind() != reflect.Array && av.Kind() != reflect.Slice
のような条件分岐で型をチェックしていましたが、新しいコードではswitch v.Kind()
を導入し、reflect.Interface
,reflect.Array
,reflect.Slice
の各ケースを明示的に処理しています。reflect.Interface
の場合:d.arrayInterface()
を呼び出して、インターフェースへのデコードを処理します。これは、ターゲットがinterface{}
型の場合に、内部的に適切なスライス型を割り当ててデコードを進めるためのものです。reflect.Array
またはreflect.Slice
の場合:break
して、後続の配列/スライスデコードロジックに進みます。default
の場合:UnmarshalTypeError
を発生させ、ターゲットが配列でもスライスでもインターフェースでもない場合にエラーを報告します。 この変更により、型チェックのロジックがより明確になり、エラーハンドリングも改善されました。
-
スライス拡張ロジックの修正: JSON配列の要素をGoのスライスにデコードする際、スライスの容量が不足した場合に動的に拡張する必要があります。以前のコードでは、
av.Cap()
やsv.IsValid()
といった変数を使ってスライスの容量や有効性をチェックしていましたが、これが正しく機能していなかった可能性があります。 新しいコードでは、if v.Kind() == reflect.Slice
でスライスであることを確認した上で、i >= v.Cap()
で容量不足をチェックし、reflect.MakeSlice
とreflect.Copy
を使って新しい、より大きなスライスを作成し、既存の要素をコピーしています。また、i >= v.Len()
で長さが不足している場合にv.SetLen(i + 1)
を呼び出してスライスの長さを適切に調整しています。これにより、JSON配列の要素数に応じてGoのスライスが正しく拡張されるようになりました。 -
配列のゼロ埋めと空スライスの初期化: デコードされたJSON配列の要素数がGoの固定長配列の要素数よりも少なかった場合、残りの要素をゼロ値で埋める必要があります。また、空のJSON配列がデコードされた場合に、Goのスライスが正しく空のスライスとして初期化される必要があります。このコミットでは、これらのエッジケースに対するロジックも修正・簡素化されています。特に、
if i == 0 && v.Kind() == reflect.Slice
の条件で、空のスライスをreflect.MakeSlice(v.Type(), 0, 0)
で正しく初期化するように変更されています。
-
これらの変更により、encoding/json
パッケージは、JSON配列をGoの配列やスライスにデコードする際の堅牢性、正確性、そしてコードの保守性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の2つです。
src/pkg/encoding/json/decode.go
src/pkg/encoding/json/decode_test.go
src/pkg/encoding/json/decode.go
の変更
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -228,7 +228,9 @@ func (d *decodeState) value(v reflect.Value) {
// Feed in an empty string - the shortest, simplest value -
// so that it knows we got to the end of the value.
if d.scan.redo {
- panic("redo")
+ // rewind.
+ d.scan.redo = false
+ d.scan.step = stateBeginValue
}
d.scan.step(&d.scan, '"')
d.scan.step(&d.scan, '"')
@@ -317,25 +319,22 @@ func (d *decodeState) array(v reflect.Value) {
}
v = pv
- // Decoding into nil interface? Switch to non-reflect code.
- iv := v
- ok := iv.Kind() == reflect.Interface
- if ok {
- iv.Set(reflect.ValueOf(d.arrayInterface()))
- return
- }
-
// Check type of target.
- av := v
- if av.Kind() != reflect.Array && av.Kind() != reflect.Slice {
+ switch v.Kind() {
+ default:
d.saveError(&UnmarshalTypeError{"array", v.Type()})
d.off--
d.next()
return
+ case reflect.Interface:
+ // Decoding into nil interface? Switch to non-reflect code.
+ v.Set(reflect.ValueOf(d.arrayInterface()))
+ return
+ case reflect.Array:
+ case reflect.Slice:
+ break
}
- sv := v
-
i := 0
for {
// Look ahead for ] - can only happen on first iteration.
@@ -349,23 +348,25 @@ func (d *decodeState) array(v reflect.Value) {
d.scan.undo(op)
// Get element of array, growing if necessary.
- if i >= av.Cap() && sv.IsValid() {
- newcap := sv.Cap() + sv.Cap()/2
- if newcap < 4 {
- newcap = 4
+ if v.Kind() == reflect.Slice {
+ // Grow slice if necessary
+ if i >= v.Cap() {
+ newcap := v.Cap() + v.Cap()/2
+ if newcap < 4 {
+ newcap = 4
+ }
+ newv := reflect.MakeSlice(v.Type(), v.Len(), newcap)
+ reflect.Copy(newv, v)
+ v.Set(newv)
}
- newv := reflect.MakeSlice(sv.Type(), sv.Len(), newcap)
- reflect.Copy(newv, sv)
- sv.Set(newv)
- }
- if i >= av.Len() && sv.IsValid() {
- // Must be slice; gave up on array during i >= av.Cap().
- sv.SetLen(i + 1)
+ if i >= v.Len() {
+ v.SetLen(i + 1)
+ }
}
// Decode into element.
- if i < av.Len() {
- d.value(av.Index(i))
+ if i < v.Len() {
+ d.value(v.Index(i))
} else {
// Ran out of fixed array: skip.
d.value(reflect.Value{})
@@ -382,19 +383,19 @@ func (d *decodeState) array(v reflect.Value) {
}
}
- if i < av.Len() {
- if !sv.IsValid() {
+ if i < v.Len() {
+ if v.Kind() == reflect.Array {
// Array. Zero the rest.
- z := reflect.Zero(av.Type().Elem())
- for ; i < av.Len(); i++ {
- av.Index(i).Set(z)
+ z := reflect.Zero(v.Type().Elem())
+ for ; i < v.Len(); i++ {
+ v.Index(i).Set(z)
}
} else {
- sv.SetLen(i)
+ v.SetLen(i)
}
}
- if i == 0 && av.Kind() == reflect.Slice && sv.IsNil() {
- sv.Set(reflect.MakeSlice(sv.Type(), 0, 0))
+ if i == 0 && v.Kind() == reflect.Slice {
+ v.Set(reflect.MakeSlice(v.Type(), 0, 0))
}
}
src/pkg/encoding/json/decode_test.go
の変更
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -74,6 +74,12 @@ var unmarshalTests = []unmarshalTest{\n
// syntax errors
{`{"X": "foo", "Y"}`, nil, nil, &SyntaxError{"invalid character '}' after object key", 17}},
+ {`[1, 2, 3+]`, nil, nil, &SyntaxError{"invalid character '+' after array element", 9}},
+
+ // array tests
+ {`[1, 2, 3]`, new([3]int), [3]int{1, 2, 3}, nil},
+ {`[1, 2, 3]`, new([1]int), [1]int{1}, nil},
+ {`[1, 2, 3]`, new([5]int), [5]int{1, 2, 3, 0, 0}, nil},
// composite tests
{allValueIndent, new(All), allValue, nil},
コアとなるコードの解説
src/pkg/encoding/json/decode.go
-
func (d *decodeState) value(v reflect.Value)
メソッド内の変更 (L228-233):- 変更前:
if d.scan.redo { panic("redo") }
- 変更後:
if d.scan.redo { // rewind. d.scan.redo = false d.scan.step = stateBeginValue }
- 解説:
d.scan.redo
は、JSONスキャン中に特定の状態を巻き戻す必要があることを示すフラグです。以前は、このフラグがtrue
の場合に無条件にパニックを起こしていました。これはデバッグ目的か、未実装のロジックだった可能性があります。この修正により、パニックを回避し、d.scan.redo
をfalse
にリセットし、スキャン状態をstateBeginValue
(値の開始状態) に戻すことで、スキャン処理が正常に続行されるようにしました。これにより、特定の不正なJSON入力が原因でデコーダがクラッシュする問題を修正したと考えられます。
- 変更前:
-
func (d *decodeState) array(v reflect.Value)
メソッド内の変更 (L317-337):- 変更前:
// Decoding into nil interface? Switch to non-reflect code. iv := v ok := iv.Kind() == reflect.Interface if ok { iv.Set(reflect.ValueOf(d.arrayInterface())) return } // Check type of target. av := v if av.Kind() != reflect.Array && av.Kind() != reflect.Slice { d.saveError(&UnmarshalTypeError{"array", v.Type()}) d.off-- d.next() return } sv := v
- 変更後:
switch v.Kind() { default: d.saveError(&UnmarshalTypeError{"array", v.Type()}) d.off-- d.next() return case reflect.Interface: // Decoding into nil interface? Switch to non-reflect code. v.Set(reflect.ValueOf(d.arrayInterface())) return case reflect.Array: case reflect.Slice: break }
- 解説:
- 不要な変数の削除:
iv
,ok
,av
,sv
といった複数のreflect.Value
型のヘルパー変数が削除されました。これらの変数は、以前のreflect
パッケージのAPI変更の残骸であり、冗長であったり、混乱を招いたりしていました。デコード対象のv reflect.Value
を直接操作することで、コードが大幅に簡素化され、可読性が向上しました。 switch v.Kind()
による型チェックの明確化: 以前はif
文で配列とスライスの型をチェックしていましたが、switch v.Kind()
を使用することで、ターゲットの型がreflect.Interface
、reflect.Array
、reflect.Slice
のいずれであるかをより明確に判別できるようになりました。reflect.Interface
の場合、d.arrayInterface()
を呼び出して、インターフェースへのデコードを処理します。これは、interface{}
型の変数にJSON配列をデコードする際に、適切なスライス型を動的に割り当てるためのものです。reflect.Array
またはreflect.Slice
の場合、break
して後続の配列/スライスデコードロジックに進みます。default
ケースでは、ターゲットが配列でもスライスでもインターフェースでもない場合にUnmarshalTypeError
を発生させ、適切なエラーハンドリングを行います。この変更により、型チェックのロジックがより堅牢になりました。
- 不要な変数の削除:
- 変更前:
-
スライス拡張ロジックの修正 (L349-369):
- 変更前:
if i >= av.Cap() && sv.IsValid() { ... }
やif i >= av.Len() && sv.IsValid() { ... }
のような条件でスライスの容量や長さをチェックし、sv
を使ってスライスを拡張していました。 - 変更後:
if v.Kind() == reflect.Slice { // Grow slice if necessary if i >= v.Cap() { newcap := v.Cap() + v.Cap()/2 if newcap < 4 { newcap = 4 } newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) reflect.Copy(newv, v) v.Set(newv) } if i >= v.Len() { v.SetLen(i + 1) } } // Decode into element. if i < v.Len() { d.value(v.Index(i)) } else { // Ran out of fixed array: skip. d.value(reflect.Value{}) }
- 解説:
- スライス拡張のロジックが
if v.Kind() == reflect.Slice
のブロック内に移動し、デコード対象のv
を直接操作するように変更されました。これにより、sv
変数の使用が不要になり、コードがより直接的になりました。 i >= v.Cap()
でスライスの容量が不足しているかをチェックし、不足していればreflect.MakeSlice
で新しいスライスを作成し、reflect.Copy
で既存の要素をコピーしてv.Set(newv)
でv
を新しいスライスに更新します。i >= v.Len()
でスライスの長さが不足しているかをチェックし、不足していればv.SetLen(i + 1)
で長さを1つ増やします。- これらの変更により、JSON配列の要素数に応じてGoのスライスが正しく動的に拡張されるようになり、以前のバグが修正されました。また、要素のデコードも
v.Index(i)
を直接使うことで簡素化されています。
- スライス拡張のロジックが
- 変更前:
-
配列のゼロ埋めと空スライスの初期化 (L382-390):
- 変更前:
if i < av.Len() { if !sv.IsValid() { ... } else { sv.SetLen(i) } }
やif i == 0 && av.Kind() == reflect.Slice && sv.IsNil() { sv.Set(reflect.MakeSlice(sv.Type(), 0, 0)) }
- 変更後:
if i < v.Len() { if v.Kind() == reflect.Array { // Array. Zero the rest. z := reflect.Zero(v.Type().Elem()) for ; i < v.Len(); i++ { v.Index(i).Set(z) } } else { v.SetLen(i) } } if i == 0 && v.Kind() == reflect.Slice { v.Set(reflect.MakeSlice(v.Type(), 0, 0)) }
- 解説:
- JSON配列の要素数がGoの固定長配列の要素数より少なかった場合、残りの要素をゼロ値で埋めるロジックが
if v.Kind() == reflect.Array
のブロック内に移動し、v
を直接操作するように変更されました。 - 空のJSON配列がデコードされた場合に、Goのスライスが正しく空のスライスとして初期化されるロジックも簡素化されました。
sv.IsNil()
のチェックが不要になり、if i == 0 && v.Kind() == reflect.Slice
の条件でv.Set(reflect.MakeSlice(v.Type(), 0, 0))
を呼び出すことで、空のスライスが正しく初期化されるようになりました。
- JSON配列の要素数がGoの固定長配列の要素数より少なかった場合、残りの要素をゼロ値で埋めるロジックが
- 変更前:
src/pkg/encoding/json/decode_test.go
- 追加されたテストケース:
{
[1, 2, 3+], nil, nil, &SyntaxError{"invalid character '+' after array element", 9}}
: 不正なJSON配列の構文エラーをテストします。{
[1, 2, 3], new([3]int), [3]int{1, 2, 3}, nil}
: JSON配列がGoの固定長配列に正しくデコードされることをテストします。{
[1, 2, 3], new([1]int), [1]int{1}, nil}
: JSON配列の要素がGoの固定長配列の容量を超える場合に、余分な要素が無視されることをテストします。{
[1, 2, 3], new([5]int), [5]int{1, 2, 3, 0, 0}, nil}
: JSON配列の要素がGoの固定長配列の容量より少ない場合に、残りの要素がゼロ値で埋められることをテストします。
これらのテストケースの追加は、修正された配列デコードロジックが期待通りに機能し、特に配列とスライスのサイズ調整やエラーハンドリングが正しく行われることを検証するために重要です。
関連リンク
- Go Issue #2513: encoding/json: array decoding broken by reflect API changes
- Gerrit Change-ID: https://golang.org/cl/5488094
参考にした情報源リンク
- Go Programming Language Specification - Arrays, slices, and maps: https://go.dev/ref/spec#Arrays
- Go Programming Language Specification - The reflect package: https://go.dev/pkg/reflect/
- Go Programming Language Specification - The encoding/json package: https://go.dev/pkg/encoding/json/
- A Little Tour of Go - The Go Blog (reflect package explanation): https://go.dev/blog/laws-of-reflection
- Go Reflect Cheat Sheet: https://yourbasic.org/golang/reflect-cheat-sheet/
- Go Slices: usage and internals: https://go.dev/blog/slices
- Go Data Structures: Arrays, Slices, and Maps: https://www.digitalocean.com/community/tutorials/understanding-arrays-slices-and-maps-in-go
- Go JSON Unmarshal: https://www.golangprograms.com/go-json-unmarshal.html
- Go reflect.Value.SetLen() example: https://pkg.go.dev/reflect#Value.SetLen
- Go reflect.MakeSlice() example: https://pkg.go.dev/reflect#MakeSlice
- Go reflect.Copy() example: https://pkg.go.dev/reflect#Copy