[インデックス 13309] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、nil ではないインターフェース値へのJSONアンマーシャリング時に発生するパニック(panic)を修正するものです。具体的には、json.Unmarshal が、既に値が設定されているインターフェース型変数(特にポインタ型をラップしている場合)に null をアンマーシャリングしようとした際に、不正なメモリアクセスを引き起こす可能性があったバグに対処しています。この修正により、nil ではないインターフェース値への安全なアンマーシャリングが保証され、堅牢性が向上しました。
コミット
commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 7 01:48:55 2012 -0400
encoding/json: fix panic unmarshaling into non-nil interface value
Fixes #3614.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6306051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26
元コミット内容
commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 7 01:48:55 2012 -0400
encoding/json: fix panic unmarshaling into non-nil interface value
Fixes #3614.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6306051
---
src/pkg/encoding/json/decode.go | 9 +++++--
src/pkg/encoding/json/decode_test.go | 46 ++++++++++++++++++++++++++++++++++++\
2 files changed, 53 insertions(+), 2 deletions(-)
diff --git a/src/pkg/encoding/json/decode.go b/src/pkg/encoding/json/decode.go
index 0018e534cc..44dc5784be 100644
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
_, isUnmarshaler = v.Interface().(Unmarshaler)\
}\
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
\t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
\t\t}\
\t\tpv := v
diff --git a/src/pkg/encoding/json/decode_test.go b/src/pkg/encoding/json/decode_test.go
index c7dce53f29..5a85e3f751 100644
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -683,3 +683,49 @@ func TestEmptyString(t *testing.T) {
t.Fatal("Decode: did not set Number1")
}\
}\
+\n+func intp(x int) *int {\
+\tp := new(int)\
+\t*p = x\
+\treturn p\
+}\
+\n+func intpp(x *int) **int {\
+\tpp := new(*int)\
+\t*pp = x\
+\treturn pp\
+}\
+\n+var interfaceSetTests = []struct {\
+\tpre interface{}\
+\tjson string\
+\tpost interface{}\
+}{\
+\t{\"foo\", `\"bar\"`, \"bar\"},\
+\t{\"foo\", `2`, 2.0},\
+\t{\"foo\", `true`, true},\
+\t{\"foo\", `null`, nil},\
+\n+\t{nil, `null`, nil},\
+\t{new(int), `null`, nil},\
+\t{(*int)(nil), `null`, nil},\
+\t{new(*int), `null`, new(*int)},\
+\t{(**int)(nil), `null`, nil},\
+\t{intp(1), `null`, nil},\
+\t{intpp(nil), `null`, intpp(nil)},\
+\t{intpp(intp(1)), `null`, intpp(nil)},\
+}\
+\n+func TestInterfaceSet(t *testing.T) {\
+\tfor _, tt := range interfaceSetTests {\
+\t\tb := struct{ X interface{} }{tt.pre}\
+\t\tblob := `{\"X\":` + tt.json + `}`\
+\t\tif err := Unmarshal([]byte(blob), &b); err != nil {\
+\t\t\tt.Errorf(\"Unmarshal %#q: %v\", blob, err)\
+\t\t\tcontinue\
+\t\t}\
+\t\tif !reflect.DeepEqual(b.X, tt.post) {\
+\t\t\tt.Errorf(\"Unmarshal %#q into %#v: X=%#v, want %#v\", blob, tt.pre, b.X, tt.post)\
+\t\t}\
+\t}\
+}\
変更の背景
このコミットは、Go言語のIssue #3614「json.Unmarshal panics when unmarshaling into non-nil interface value」を修正するために行われました。
このバグは、encoding/json パッケージの Unmarshal 関数が、既に値が設定されている(nil ではない)インターフェース変数に対してJSONの null 値をデコードしようとした際に発生しました。特に、インターフェースがポインタ型(例: *int や **int)をラップしている場合に問題が顕在化しました。
従来の Unmarshal の動作では、インターフェースが nil でない場合、そのインターフェースが保持している具体的な値(Elem() で取得できる)に対して直接デコードを試みていました。しかし、JSONの null はGoの nil に対応するため、ポインタ型の値に null をデコードしようとすると、そのポインタ自体を nil に設定する必要があります。
問題は、reflect.Value.Set(reflect.Zero(v.Type())) のような操作が、アドレス可能でない(CanSet() が false の)reflect.Value に対して行われた場合にパニックを引き起こす点にありました。インターフェースがラップしているポインタが、さらに別のポインタを指しているような多重ポインタの場合、Elem() を辿った先の値がアドレス可能でないことがあり、そこに nil を設定しようとするとパニックが発生していました。
この修正は、このような特定のシナリオ、特に null をデコードする際に、インターフェースがラップしている値が「有用にアドレス可能」であるかどうかをより厳密にチェックすることで、パニックを回避することを目的としています。
前提知識の解説
Go言語の encoding/json パッケージ
encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。
json.Marshal: Goのデータ構造をJSONバイト列にエンコードします。json.Unmarshal: JSONバイト列をGoのデータ構造にデコードします。
Go言語の interface{} 型
interface{} はGoにおける空のインターフェース型で、あらゆる型の値を保持できます。Goのインターフェースは、内部的に「型」と「値」のペアとして表現されます。
nilインターフェース: 型も値もnilの状態。nilではないインターフェース: 型は存在するが、値がnilの状態(例:var i interface{} = (*int)(nil))。この状態が今回のバグの核心でした。
Go言語の reflect パッケージ
reflect パッケージは、実行時にGoの型情報や値情報を検査・操作するための機能を提供します。
reflect.ValueOf(i interface{}) reflect.Value: インターフェース値iのreflect.Value表現を返します。reflect.Value.Kind() reflect.Kind:reflect.Valueが表す値の具体的な種類(例:reflect.Int,reflect.Ptr,reflect.Interfaceなど)を返します。reflect.Value.Elem() reflect.Value: ポインタ、インターフェース、またはスライスが指す要素のreflect.Valueを返します。ポインタの場合、そのポインタが指す先の値のreflect.Valueを返します。インターフェースの場合、そのインターフェースが保持している具体的な値のreflect.Valueを返します。reflect.Value.IsNil() bool:reflect.Valueがnilであるかどうかをチェックします。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。reflect.Value.CanSet() bool:reflect.Valueが変更可能(アドレス可能)であるかどうかをチェックします。CanSet()がtrueでないと、Set()メソッドなどで値を変更することはできません。
JSONの null とGoの nil
JSONの null は、Goの nil 値にデコードされます。これは、ポインタ、スライス、マップ、インターフェース、チャネル、関数などの参照型に適用されます。
技術的詳細
このバグは、encoding/json のデコード処理の中核を担う decodeState.indirect メソッドに存在していました。このメソッドは、reflect.Value を受け取り、それがポインタやインターフェースである場合に、その「実体」を辿ってデコード可能な reflect.Value を取得する役割を担っています。
問題のコードは以下の部分でした。
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
v = iv.Elem()
continue
}
このコードは、v が nil ではないインターフェースである場合、そのインターフェースが保持している具体的な値(iv.Elem())を v に再代入し、ループを続行していました。
しかし、iv.Elem() が返す reflect.Value は、必ずしもアドレス可能であるとは限りません。特に、interface{} が **int のような多重ポインタを保持しており、その **int が nil である場合、iv.Elem() は *int 型の reflect.Value を返しますが、これは nil ポインタであり、かつアドレス可能ではありません。
JSONの null をデコードする際、encoding/json は最終的に reflect.Value.Set(reflect.Zero(v.Type())) のような操作でターゲットの値を nil に設定しようとします。もし v がアドレス可能でない reflect.Value であった場合、Set メソッドはパニックを引き起こします。
修正は、この iv.Elem() を取得した後の v の再代入に条件を追加することで、このパニックを回避しています。
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
e := iv.Elem() // インターフェースが保持する具体的な値
// e がポインタであり、かつ nil ではない場合、または
// null をデコード中で、かつ e が指す先がポインタである場合
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
v = e // e を新しい v として続行
continue
}
}
この新しい条件 (!decodingNull || e.Elem().Kind() == reflect.Ptr) が重要です。
!decodingNull:nullをデコードしていない場合、つまり通常の値をデコードしている場合は、以前と同様にeをvとして続行します。decodingNull && e.Elem().Kind() == reflect.Ptr:nullをデコードしており、かつeが指す先(e.Elem())がポインタである場合もeをvとして続行します。これは、**intのようなケースで、eが*intであり、そのElem()がintではなく*intである場合に、さらにそのポインタを辿ってnilを設定できるようにするためです。
この条件により、null をデコードする際に、v が実際に nil を設定できるアドレス可能なポインタ型である場合にのみ Elem() を辿るように制御し、それ以外の場合は現在の v のまま処理を続行することで、パニックを回避しています。
テストケース TestInterfaceSet は、この修正が正しく機能することを確認するために追加されました。特に、new(*int), intpp(nil), intpp(intp(1)) のような多重ポインタや nil ポインタを含むインターフェースへの null デコードが正しく処理されることを検証しています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go の decodeState.indirect メソッド内の if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() ブロックが変更されました。
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
_, isUnmarshaler = v.Interface().(Unmarshaler)\
}\
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
\t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
\t\t}\
\t\tpv = v
また、src/pkg/encoding/json/decode_test.go に TestInterfaceSet という新しいテスト関数と関連するヘルパー関数 (intp, intpp) およびテストデータ (interfaceSetTests) が追加されました。
コアとなるコードの解説
変更された decodeState.indirect メソッドのコードは、json.Unmarshal がインターフェース値を処理する際のロジックを改善しています。
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
e := iv.Elem() // インターフェースが保持する具体的な値を取得
// Load value from interface, but only if the result will be
// usefully addressable.
// e がポインタであり、かつ nil ではない場合、
// さらに以下の条件のいずれかを満たす場合にのみ、e を v として処理を続行する:
// 1. decodingNull が false (null 以外の値をデコード中)
// 2. decodingNull が true (null をデコード中) かつ e.Elem() がポインタ型である
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
v = e // e を新しい v として、さらに間接参照を辿る
continue
}
}
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil():vがインターフェース型であり、かつnilではない(つまり、型情報と値が設定されているが、その値自体はnilかもしれない)場合にこのブロックに入ります。
e := iv.Elem():- インターフェース
ivが保持している具体的な値のreflect.Valueをeに代入します。例えば、interface{}((*int)(nil))の場合、eは*int型のreflect.Valueになります。
- インターフェース
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr):- これが新しい条件分岐です。
e.Kind() == reflect.Ptr:eがポインタ型であること。!e.IsNil():eが指すポインタ自体がnilではないこと。(!decodingNull || e.Elem().Kind() == reflect.Ptr): この部分がnullデコード時のパニックを防ぐための肝です。!decodingNull: もし現在null以外の値をデコードしているのであれば、この条件はtrueになり、以前と同様にeをvとして処理を続行します。これは、null以外の値をデコードする際には、eがアドレス可能であれば問題ないためです。decodingNull || e.Elem().Kind() == reflect.Ptr: もし現在nullをデコードしているのであれば、e.Elem().Kind() == reflect.Ptrがtrueである場合にのみ、eをvとして処理を続行します。これは、**intのような多重ポインタの場合に重要です。eが*intであり、そのElem()がさらにポインタ(intではなく*int)である場合、nullを設定するためにはさらにそのポインタを辿る必要があるためです。これにより、nilを設定する対象が適切にアドレス可能なポインタであることを保証します。
この修正により、json.Unmarshal は、nil ではないインターフェース値に null をデコードする際に、不適切にアドレス可能でない reflect.Value に対して Set 操作を行おうとすることを防ぎ、パニックを回避できるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26
- Go Issue #3614: https://go.dev/issue/3614
- Go CL 6306051: https://golang.org/cl/6306051
参考にした情報源リンク
- Go Issue Tracker (Issue #3614): https://go.dev/issue/3614
- Go
reflectパッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go
encoding/jsonパッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go Code Review (CL 6306051): https://golang.org/cl/6306051
- Go言語のインターフェースとnilについて: https://go.dev/blog/laws-of-reflection (Laws of Reflection)```markdown
[インデックス 13309] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、nil ではないインターフェース値へのJSONアンマーシャリング時に発生するパニック(panic)を修正するものです。具体的には、json.Unmarshal が、既に値が設定されているインターフェース型変数(特にポインタ型をラップしている場合)に null をアンマーシャリングしようとした際に、不正なメモリアクセスを引き起こす可能性があったバグに対処しています。この修正により、nil ではないインターフェース値への安全なアンマーシャリングが保証され、堅牢性が向上しました。
コミット
commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 7 01:48:55 2012 -0400
encoding/json: fix panic unmarshaling into non-nil interface value
Fixes #3614.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6306051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26
元コミット内容
commit 09b736a2ab56ee520e3f5909c09c8417fe61db26
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 7 01:48:55 2012 -0400
encoding/json: fix panic unmarshaling into non-nil interface value
Fixes #3614.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6306051
---
src/pkg/encoding/json/decode.go | 9 +++++--
src/pkg/encoding/json/decode_test.go | 46 ++++++++++++++++++++++++++++++++++++\
2 files changed, 53 insertions(+), 2 deletions(-)
diff --git a/src/pkg/encoding/json/decode.go b/src/pkg/encoding/json/decode.go
index 0018e534cc..44dc5784be 100644
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
_, isUnmarshaler = v.Interface().(Unmarshaler)\
}\
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
\t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
\t\t}\
\t\tpv = v
diff --git a/src/pkg/encoding/json/decode_test.go b/src/pkg/encoding/json/decode_test.go
index c7dce53f29..5a85e3f751 100644
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -683,3 +683,49 @@ func TestEmptyString(t *testing.T) {
t.Fatal("Decode: did not set Number1")
}\
}\
+\n+func intp(x int) *int {\
+\tp := new(int)\
+\t*p = x\
+\treturn p\
+}\
+\n+func intpp(x *int) **int {\
+\tpp := new(*int)\
+\t*pp = x\
+\treturn pp\
+}\
+\n+var interfaceSetTests = []struct {\
+\tpre interface{}\
+\tjson string\
+\tpost interface{}\
+}{\
+\t{\"foo\", `\"bar\"`, \"bar\"},\
+\t{\"foo\", `2`, 2.0},\
+\t{\"foo\", `true`, true},\
+\t{\"foo\", `null`, nil},\
+\n+\t{nil, `null`, nil},\
+\t{new(int), `null`, nil},\
+\t{(*int)(nil), `null`, nil},\
+\t{new(*int), `null`, new(*int)},\
+\t{(**int)(nil), `null`, nil},\
+\t{intp(1), `null`, nil},\
+\t{intpp(nil), `null`, intpp(nil)},\
+\t{intpp(intp(1)), `null`, intpp(nil)},\
+}\
+\n+func TestInterfaceSet(t *testing.T) {\
+\tfor _, tt := range interfaceSetTests {\
+\t\tb := struct{ X interface{} }{tt.pre}\
+\t\tblob := `{\"X\":` + tt.json + `}`\
+\t\tif err := Unmarshal([]byte(blob), &b); err != nil {\
+\t\t\tt.Errorf(\"Unmarshal %#q: %v\", blob, err)\
+\t\t\tcontinue\
+\t\t}\
+\t\tif !reflect.DeepEqual(b.X, tt.post) {\
+\t\t\tt.Errorf(\"Unmarshal %#q into %#v: X=%#v, want %#v\", blob, tt.pre, b.X, tt.post)\
+\t\t}\
+\t}\
+}\
変更の背景
このコミットは、Go言語のIssue #3614「json.Unmarshal panics when unmarshaling into non-nil interface value」を修正するために行われました。
このバグは、encoding/json パッケージの Unmarshal 関数が、既に値が設定されている(nil ではない)インターフェース変数に対してJSONの null 値をデコードしようとした際に発生しました。特に、インターフェースがポインタ型(例: *int や **int)をラップしている場合に問題が顕在化しました。
従来の Unmarshal の動作では、インターフェースが nil でない場合、そのインターフェースが保持している具体的な値(Elem() で取得できる)に対して直接デコードを試みていました。しかし、JSONの null はGoの nil に対応するため、ポインタ型の値に null をデコードしようとすると、そのポインタ自体を nil に設定する必要があります。
問題は、reflect.Value.Set(reflect.Zero(v.Type())) のような操作が、アドレス可能でない(CanSet() が false の)reflect.Value に対して行われた場合にパニックを引き起こす点にありました。インターフェースがラップしているポインタが、さらに別のポインタを指しているような多重ポインタの場合、Elem() を辿った先の値がアドレス可能でないことがあり、そこに nil を設定しようとするとパニックが発生していました。
この修正は、このような特定のシナリオ、特に null をデコードする際に、インターフェースがラップしている値が「有用にアドレス可能」であるかどうかをより厳密にチェックすることで、パニックを回避することを目的としています。
前提知識の解説
Go言語の encoding/json パッケージ
encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。
json.Marshal: Goのデータ構造をJSONバイト列にエンコードします。json.Unmarshal: JSONバイト列をGoのデータ構造にデコードします。
Go言語の interface{} 型
interface{} はGoにおける空のインターフェース型で、あらゆる型の値を保持できます。Goのインターフェースは、内部的に「型」と「値」のペアとして表現されます。
nilインターフェース: 型も値もnilの状態。nilではないインターフェース: 型は存在するが、値がnilの状態(例:var i interface{} = (*int)(nil))。この状態が今回のバグの核心でした。
Go言語の reflect パッケージ
reflect パッケージは、実行時にGoの型情報や値情報を検査・操作するための機能を提供します。
reflect.ValueOf(i interface{}) reflect.Value: インターフェース値iのreflect.Value表現を返します。reflect.Value.Kind() reflect.Kind:reflect.Valueが表す値の具体的な種類(例:reflect.Int,reflect.Ptr,reflect.Interfaceなど)を返します。reflect.Value.Elem() reflect.Value: ポインタ、インターフェース、またはスライスが指す要素のreflect.Valueを返します。ポインタの場合、そのポインタが指す先の値のreflect.Valueを返します。インターフェースの場合、そのインターフェースが保持している具体的な値のreflect.Valueを返します。reflect.Value.IsNil() bool:reflect.Valueがnilであるかどうかをチェックします。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。reflect.Value.CanSet() bool:reflect.Valueが変更可能(アドレス可能)であるかどうかをチェックします。CanSet()がtrueでないと、Set()メソッドなどで値を変更することはできません。
JSONの null とGoの nil
JSONの null は、Goの nil 値にデコードされます。これは、ポインタ、スライス、マップ、インターフェース、チャネル、関数などの参照型に適用されます。
技術的詳細
このバグは、encoding/json のデコード処理の中核を担う decodeState.indirect メソッドに存在していました。このメソッドは、reflect.Value を受け取り、それがポインタやインターフェースである場合に、その「実体」を辿ってデコード可能な reflect.Value を取得する役割を担っています。
問題のコードは以下の部分でした。
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
v = iv.Elem()
continue
}
このコードは、v が nil ではないインターフェースである場合、そのインターフェースが保持している具体的な値(iv.Elem())を v に再代入し、ループを続行していました。
しかし、iv.Elem() が返す reflect.Value は、必ずしもアドレス可能であるとは限りません。特に、interface{} が **int のような多重ポインタを保持しており、その **int が nil である場合、iv.Elem() は *int 型の reflect.Value を返しますが、これは nil ポインタであり、かつアドレス可能ではありません。
JSONの null をデコードする際、encoding/json は最終的に reflect.Value.Set(reflect.Zero(v.Type())) のような操作でターゲットの値を nil に設定しようとします。もし v がアドレス可能でない reflect.Value であった場合、Set メソッドはパニックを引き起こします。
修正は、この iv.Elem() を取得した後の v の再代入に条件を追加することで、このパニックを回避しています。
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
e := iv.Elem() // インターフェースが保持する具体的な値
// e がポインタであり、かつ nil ではない場合、または
// null をデコード中で、かつ e が指す先がポインタである場合
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
v = e // e を新しい v として続行
continue
}
}
この新しい条件 (!decodingNull || e.Elem().Kind() == reflect.Ptr) が重要です。
!decodingNull:nullをデコードしていない場合、つまり通常の値をデコードしている場合は、以前と同様にeをvとして続行します。decodingNull && e.Elem().Kind() == reflect.Ptr:nullをデコードしており、かつeが指す先(e.Elem())がポインタである場合もeをvとして続行します。これは、**intのようなケースで、eが*intであり、そのElem()がintではなく*intである場合に、さらにそのポインタを辿ってnilを設定できるようにするためです。
この条件により、null をデコードする際に、v が実際に nil を設定できるアドレス可能なポインタ型である場合にのみ Elem() を辿るように制御し、それ以外の場合は現在の v のまま処理を続行することで、パニックを回避しています。
テストケース TestInterfaceSet は、この修正が正しく機能することを確認するために追加されました。特に、new(*int), intpp(nil), intpp(intp(1)) のような多重ポインタや nil ポインタを含むインターフェースへの null デコードが正しく処理されることを検証しています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go の decodeState.indirect メソッド内の if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() ブロックが変更されました。
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -273,9 +273,14 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler,\
_, isUnmarshaler = v.Interface().(Unmarshaler)\
}\
+\t\t// Load value from interface, but only if the result will be\
+\t\t// usefully addressable.\
\t\tif iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {\
-\t\t\tv = iv.Elem()\
-\t\t\tcontinue\
+\t\t\te := iv.Elem()\
+\t\t\tif e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {\
+\t\t\t\tv = e\
+\t\t\t\tcontinue\
+\t\t\t}\
\t\t}\
\t\tpv = v
また、src/pkg/encoding/json/decode_test.go に TestInterfaceSet という新しいテスト関数と関連するヘルパー関数 (intp, intpp) およびテストデータ (interfaceSetTests) が追加されました。
コアとなるコードの解説
変更された decodeState.indirect メソッドのコードは、json.Unmarshal がインターフェース値を処理する際のロジックを改善しています。
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil() {
e := iv.Elem() // インターフェースが保持する具体的な値を取得
// Load value from interface, but only if the result will be
// usefully addressable.
// e がポインタであり、かつ nil ではない場合、
// さらに以下の条件のいずれかを満たす場合にのみ、e を v として処理を続行する:
// 1. decodingNull が false (null 以外の値をデコード中)
// 2. decodingNull が true (null をデコード中) かつ e.Elem() がポインタ型である
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) {
v = e // e を新しい v として、さらに間接参照を辿る
continue
}
}
if iv := v; iv.Kind() == reflect.Interface && !iv.IsNil():vがインターフェース型であり、かつnilではない(つまり、型情報と値が設定されているが、その値自体はnilかもしれない)場合にこのブロックに入ります。
e := iv.Elem():- インターフェース
ivが保持している具体的な値のreflect.Valueをeに代入します。例えば、interface{}((*int)(nil))の場合、eは*int型のreflect.Valueになります。
- インターフェース
if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr):- これが新しい条件分岐です。
e.Kind() == reflect.Ptr:eがポインタ型であること。!e.IsNil():eが指すポインタ自体がnilではないこと。(!decodingNull || e.Elem().Kind() == reflect.Ptr): この部分がnullデコード時のパニックを防ぐための肝です。!decodingNull: もし現在null以外の値をデコードしているのであれば、この条件はtrueになり、以前と同様にeをvとして処理を続行します。これは、null以外の値をデコードする際には、eがアドレス可能であれば問題ないためです。decodingNull || e.Elem().Kind() == reflect.Ptr: もし現在nullをデコードしているのであれば、e.Elem().Kind() == reflect.Ptrがtrueである場合にのみ、eをvとして処理を続行します。これは、**intのような多重ポインタの場合に重要です。eが*intであり、そのElem()がさらにポインタ(intではなく*int)である場合、nullを設定するためにはさらにそのポインタを辿る必要があるためです。これにより、nilを設定する対象が適切にアドレス可能なポインタであることを保証します。
この修正により、json.Unmarshal は、nil ではないインターフェース値に null をデコードする際に、不適切にアドレス可能でない reflect.Value に対して Set 操作を行おうとすることを防ぎ、パニックを回避できるようになりました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/09b736a2ab56ee520e3f5909c09c8417fe61db26
- Go Issue #3614: https://go.dev/issue/3614
- Go CL 6306051: https://golang.org/cl/6306051
参考にした情報源リンク
- Go Issue Tracker (Issue #3614): https://go.dev/issue/3614
- Go
reflectパッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go
encoding/jsonパッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go Code Review (CL 6306051): https://golang.org/cl/6306051
- Go言語のインターフェースとnilについて: https://go.dev/blog/laws-of-reflection (Laws of Reflection)