Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14882] ファイルの概要

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、型ミスマッチによるパニック(panic)を修正するものです。具体的には、JSONデコード時にGoのインターフェース型への値の割り当てが不適切に行われる場合に発生していた問題に対処しています。特に、reflect.Interface 型の変数がメソッドを持たない(つまり、nil インターフェースまたは空のインターフェース)場合にのみ、非リフレクションベースの高速なデコードパスを使用するように変更し、それ以外の場合は型ミスマッチとしてエラーを報告するように改善されています。これにより、予期せぬパニックを防ぎ、より堅牢なJSONデコード処理を実現しています。

コミット

  • コミットハッシュ: 406ca3c2f19a0742a05e5837ca9cae77fb4cadd8
  • 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
  • コミット日時: Mon Jan 14 08:44:16 2013 +0100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/406ca3c2f19a0742a05e5837ca9cae77fb4cadd8

元コミット内容

encoding/json: fix panics on type mismatches.

Fixes #4222.
Fixes #4628.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/7100049

変更の背景

このコミットは、Goの encoding/json パッケージが特定の状況下で型ミスマッチに遭遇した際にパニックを引き起こす問題を解決するために導入されました。具体的には、以下の2つのIssueを修正しています。

  • Issue 4222: json.Unmarshal panics when unmarshaling into error type: この問題は、json.Unmarshal を使用してJSONデータをGoの error 型の変数にデコードしようとした際にパニックが発生するというものでした。error 型はGoのインターフェース型であり、encoding/json パッケージが reflect パッケージを使って型情報を処理する際に、特定のインターフェース型(特にメソッドを持つインターフェース)への不適切なデコード試行がパニックにつながっていました。例えば、JSONのオブジェクト {} や配列 []error 型にデコードしようとするとパニックが発生していました。

  • Issue 4628: json.Unmarshal panics when unmarshaling into *string with {"user": "name"}: この問題は、JSONオブジェクトを *string 型(文字列へのポインタ)にデコードしようとした際にパニックが発生するというものでした。encoding/json は、JSONオブジェクトを文字列にデコードしようとすると型ミスマッチとして UnmarshalTypeError を返すのが正しい挙動ですが、このケースではパニックが発生していました。これは、reflect.Interface 型の処理ロジックが、特定の条件下で誤ったデコードパスを選択していたことに起因します。

これらのパニックは、encoding/json パッケージの堅牢性を損なうものであり、開発者が予期せぬクラッシュに遭遇する原因となっていました。このコミットは、reflect パッケージを用いた型チェックとデコードロジックを改善することで、これらのパニックを修正し、より適切なエラーハンドリング(UnmarshalTypeError の返却)を行うことを目的としています。

前提知識の解説

このコミットの理解には、以下のGo言語の概念と encoding/json パッケージの内部動作に関する知識が役立ちます。

  1. Go言語の interface{} (空インターフェース): interface{} は、Go言語における「任意の型」を表す特別なインターフェースです。これはメソッドを一切持たないため、どのような型の値でも interface{} 型の変数に代入できます。encoding/json パッケージでは、デコード先の型が interface{} の場合、JSONのデータ型(文字列、数値、ブール値、配列、オブジェクト)に応じて適切なGoの型(string, float64, bool, []interface{}, map[string]interface{})に動的に変換して格納します。

  2. Go言語の reflect パッケージ: reflect パッケージは、Goプログラムが実行時に自身の構造(型、値、メソッドなど)を検査・操作するための機能を提供します。encoding/json のような汎用的なデータシリアライズ/デシリアライズライブラリは、この reflect パッケージを多用して、デコード先のGoの型情報を取得し、JSONデータをその型に合わせて適切に変換・格納します。

    • reflect.Value: Goの変数の実行時の値を表します。
    • reflect.Kind(): reflect.Value が表す値の基本的な種類(reflect.Int, reflect.String, reflect.Slice, reflect.Interface など)を返します。
    • reflect.NumMethod(): reflect.Value が表す型が持つメソッドの数を返します。インターフェース型の場合、これはそのインターフェースが定義するメソッドの数を意味します。interface{} (空インターフェース) の場合、NumMethod()0 を返します。
  3. encoding/json パッケージのデコード処理: encoding/json パッケージの Unmarshal 関数は、JSONバイト列をGoのデータ構造にデコードします。この際、デコード先のGoの型が interface{} の場合、encoding/json は内部的に arrayInterface()objectInterface() といった非リフレクションベースの最適化されたデコードパスを使用することがあります。これは、interface{} へのデコードは型が事前に決まっていないため、リフレクションのオーバーヘッドを避けて直接Goのプリミティブ型や map[string]interface{}[]interface{} に変換できるためです。

  4. UnmarshalTypeError: encoding/json パッケージが、JSONのデータ型とGoのデコード先の型が一致しない場合に返すエラー型です。例えば、JSONの文字列 "hello" をGoの int 型にデコードしようとした場合などに発生します。このエラーは、パニックではなく、プログラムで捕捉して適切に処理できるエラーとして設計されています。

このコミットの核心は、reflect.Interface 型の変数をデコードする際に、そのインターフェースが「メソッドを持たない空インターフェース (interface{})」なのか、それとも「特定のメソッドを持つインターフェース(例: error 型)」なのかを v.NumMethod() == 0 で厳密に区別し、それに応じてデコードロジックを分岐させる点にあります。これにより、空インターフェースの場合のみ最適化されたパスを使い、それ以外のインターフェース型への不適切なデコード試行は UnmarshalTypeError として適切にエラー報告されるようになります。

技術的詳細

このコミットの技術的変更は、主に src/pkg/encoding/json/decode.go ファイル内の decodeState 構造体のメソッド、特に array, object, literalStore に集中しています。これらのメソッドは、JSONの配列、オブジェクト、およびリテラル値(真偽値、文字列、数値)をGoの対応する型にデコードする役割を担っています。

変更の核心は、デコード先のGoの型が reflect.Interface である場合の処理ロジックの修正です。

  1. decodeState.array(v reflect.Value) メソッドの変更: JSON配列をGoの型にデコードする際の処理です。 変更前は、reflect.Interface 型へのデコードの場合、無条件に d.arrayInterface() を呼び出して非リフレクションベースのデコードを行っていました。 変更後は、reflect.Interface 型の場合に加えて、v.NumMethod() == 0 という条件が追加されました。これは、デコード先のインターフェースがメソッドを持たない(つまり interface{} 型である)場合にのみ、d.arrayInterface() を呼び出すようにします。 もしインターフェースがメソッドを持つ(例: error 型)場合、fallthrough を使って default ケースに処理を移し、UnmarshalTypeError を発生させるように変更されました。これにより、JSON配列を error 型のようなメソッドを持つインターフェースにデコードしようとした際に、パニックではなく適切なエラーが返されるようになります。

  2. decodeState.object(v reflect.Value) メソッドの変更: JSONオブジェクトをGoの型にデコードする際の処理です。 変更前は、v.Kind() == reflect.Interface であれば無条件に d.objectInterface() を呼び出していました。 変更後は、v.Kind() == reflect.Interface && v.NumMethod() == 0 という条件に厳格化されました。これにより、JSONオブジェクトをデコードする際に、デコード先のインターフェースが interface{} 型である場合にのみ、最適化された d.objectInterface() パスが使用されます。それ以外のメソッドを持つインターフェース型へのデコード試行は、default ケースで UnmarshalTypeError を発生させるように変更されました。

  3. decodeState.literalStore(item []byte, v reflect.Value, fromQuoted bool) メソッドの変更: JSONのリテラル値(真偽値、文字列、数値)をGoの型にデコードする際の処理です。 このメソッドは、JSONの真偽値、文字列、数値のそれぞれについて、デコード先のGoの型が reflect.Interface である場合の処理を修正しています。

    • 真偽値 (case 't', case 'f'): 変更前は、reflect.Interface 型であれば無条件に v.Set(reflect.ValueOf(value)) で値をセットしていました。 変更後は、v.NumMethod() == 0 の条件が追加され、空インターフェースの場合のみ値をセットします。それ以外の場合は UnmarshalTypeError を発生させます。
    • 文字列 (case '"'): 変更前は、reflect.Interface 型であれば無条件に v.Set(reflect.ValueOf(string(s))) で値をセットしていました。 変更後は、v.NumMethod() == 0 の条件が追加され、空インターフェースの場合のみ値をセットします。それ以外の場合は UnmarshalTypeError を発生させます。
    • 数値 (default): 変更前は、reflect.Interface 型であれば無条件に v.Set(reflect.ValueOf(n)) で値をセットしていました。 変更後は、v.NumMethod() != 0 の条件が追加され、メソッドを持つインターフェース型の場合に UnmarshalTypeError を発生させます。

これらの変更により、encoding/json は、デコード先のインターフェースが interface{} (メソッドを持たない空インターフェース) である場合にのみ、そのJSONの型に応じたGoの具体的な型(string, float64, bool, []interface{}, map[string]interface{})にデコードします。それ以外の、特定のメソッドを持つインターフェース型(例: error 型)に対して、JSONのデータ型がそのインターフェースの期待する型と一致しない場合(例: JSONオブジェクトを error 型にデコードしようとする場合)は、パニックではなく UnmarshalTypeError を返すように修正されました。

テストファイルの変更 (src/pkg/encoding/json/decode_test.go): 新しいテストケース decodeTypeErrorTests が追加され、TestUnmarshalTypeError 関数でこれらの修正が正しく機能するか検証しています。

  • {new(string), {"user": "name"}}: Issue 4628 のケースをカバーし、JSONオブジェクトを *string にデコードしようとした際に UnmarshalTypeError が発生することを確認します。
  • {new(error), {}}, {new(error), []}, {new(error), ""}, {new(error), 123}, {new(error), true}: Issue 4222 のケースをカバーし、様々なJSONデータ型を error 型にデコードしようとした際に UnmarshalTypeError が発生することを確認します。

これらのテストは、以前パニックを引き起こしていたシナリオが、適切に UnmarshalTypeError を返すようになったことを保証します。

コアとなるコードの変更箇所

diff --git a/src/pkg/encoding/json/decode.go b/src/pkg/encoding/json/decode.go
index d86fd7711b..eb8c75b24a 100644
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -347,15 +347,19 @@ func (d *decodeState) array(v reflect.Value) {
 
 	// Check type of target.
 	switch v.Kind() {
+\tcase reflect.Interface:
+\t\tif v.NumMethod() == 0 {
+\t\t\t// Decoding into nil interface?  Switch to non-reflect code.
+\t\t\tv.Set(reflect.ValueOf(d.arrayInterface()))
+\t\t\treturn
+\t\t}
+\t\t// Otherwise it\'s invalid.
+\t\tfallthrough
 \tdefault:
 \t\td.saveError(&UnmarshalTypeError{\"array\", v.Type()})\
 \t\td.off--
 \t\td.next()\
 \t\treturn
-\tcase reflect.Interface:\n-\t\t// Decoding into nil interface?  Switch to non-reflect code.\n-\t\tv.Set(reflect.ValueOf(d.arrayInterface()))\n-\t\treturn
 \tcase reflect.Array:\n \tcase reflect.Slice:\n \t\tbreak
@@ -441,7 +445,7 @@ func (d *decodeState) object(v reflect.Value) {
 \tv = pv
 
 	// Decoding into nil interface?  Switch to non-reflect code.
-\tif v.Kind() == reflect.Interface {\n+\tif v.Kind() == reflect.Interface && v.NumMethod() == 0 {\n \t\tv.Set(reflect.ValueOf(d.objectInterface()))\n \t\treturn
 \t}
@@ -459,11 +463,9 @@ func (d *decodeState) object(v reflect.Value) {
 \t\t\tv.Set(reflect.MakeMap(t))\n \t\t}\n \tcase reflect.Struct:\n+\n \tdefault:\n \t\td.saveError(&UnmarshalTypeError{\"object\", v.Type()})\n-\t}\n-\n-\tif !v.IsValid() {\n \t\td.off--\n \t\td.next() // skip over { } in input\n \t\treturn
@@ -646,7 +648,11 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool\n \t\tcase reflect.Bool:\n \t\t\tv.SetBool(value)\n \t\tcase reflect.Interface:\n-\t\t\tv.Set(reflect.ValueOf(value))\n+\t\t\tif v.NumMethod() == 0 {\n+\t\t\t\tv.Set(reflect.ValueOf(value))\n+\t\t\t} else {\n+\t\t\t\td.saveError(&UnmarshalTypeError{\"bool\", v.Type()})\n+\t\t\t}\n \t\t}\n \n \tcase \'\"\': // string\n@@ -676,7 +682,11 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool\n \t\tcase reflect.String:\n \t\t\tv.SetString(string(s))\n \t\tcase reflect.Interface:\n-\t\t\tv.Set(reflect.ValueOf(string(s)))\n+\t\t\tif v.NumMethod() == 0 {\n+\t\t\t\tv.Set(reflect.ValueOf(string(s)))\n+\t\t\t} else {\n+\t\t\t\td.saveError(&UnmarshalTypeError{\"string\", v.Type()})\n+\t\t\t}\n \t\t}\n \n \tdefault: // number\n@@ -705,6 +715,10 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool\n \t\t\t\td.saveError(err)\n \t\t\t\tbreak\n \t\t\t}\n+\t\t\tif v.NumMethod() != 0 {\n+\t\t\t\td.saveError(&UnmarshalTypeError{\"number\", v.Type()})\n+\t\t\t\tbreak\n+\t\t\t}\n \t\t\tv.Set(reflect.ValueOf(n))\n \n \t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\ndiff --git a/src/pkg/encoding/json/decode_test.go b/src/pkg/encoding/json/decode_test.go
index 562b5b5d88..4c75f19f4a 100644
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -1042,3 +1042,25 @@ func TestStringKind(t *testing.T) {\n \t}\n \n }\n+\n+var decodeTypeErrorTests = []struct {\n+\tdest interface{}\n+\tsrc  string\n+}{\n+\t{new(string), `{\"user\": \"name\"}`}, // issue 4628.\n+\t{new(error), `{}`},                // issue 4222\n+\t{new(error), `[]`},\n+\t{new(error), `\"\"`},\n+\t{new(error), `123`},\n+\t{new(error), `true`},\n+}\n+\n+func TestUnmarshalTypeError(t *testing.T) {\n+\tfor _, item := range decodeTypeErrorTests {\n+\t\terr := Unmarshal([]byte(item.src), item.dest)\n+\t\tif _, ok := err.(*UnmarshalTypeError); !ok {\n+\t\t\tt.Errorf(\"expected type error for Unmarshal(%q, type %T): got %v instead\",\n+\t\t\t\titem.src, item.dest, err)\n+\t\t}\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/pkg/encoding/json/decode.go`

1.  **`func (d *decodeState) array(v reflect.Value)`**:
    *   **変更前**: `reflect.Interface` のケースが独立しており、無条件に `d.arrayInterface()` を呼び出していました。これは、メソッドを持つインターフェース(例: `error`)にJSON配列をデコードしようとした場合でも、空インターフェースと同様に処理しようとし、結果としてパニックを引き起こす可能性がありました。
    *   **変更後**:
        ```go
        case reflect.Interface:
            if v.NumMethod() == 0 {
                // Decoding into nil interface?  Switch to non-reflect code.
                v.Set(reflect.ValueOf(d.arrayInterface()))
                return
            }
            // Otherwise it's invalid.
            fallthrough
        default:
            d.saveError(&UnmarshalTypeError{"array", v.Type()})
            d.off--
            d.next()
            return
        ```
        `reflect.Interface` のケースが `default` の前に移動し、`v.NumMethod() == 0` という条件が追加されました。
        *   `v.NumMethod() == 0` の場合: これはデコード先のインターフェースが `interface{}` (空インターフェース) であることを意味します。この場合のみ、最適化された非リフレクションコードパスである `d.arrayInterface()` が呼び出され、JSON配列は `[]interface{}` にデコードされます。
        *   `v.NumMethod() != 0` の場合: これはデコード先のインターフェースがメソッドを持つ(例: `error` 型)ことを意味します。この場合、`fallthrough` を使って `default` ケースに処理を移します。`default` ケースでは、`UnmarshalTypeError` が生成され、JSON配列をメソッドを持つインターフェースにデコードしようとした型ミスマッチとして適切にエラーが報告されます。これにより、パニックが回避されます。

2.  **`func (d *decodeState) object(v reflect.Value)`**:
    *   **変更前**: `if v.Kind() == reflect.Interface { ... }` という条件で、無条件に `d.objectInterface()` を呼び出していました。これも `array` メソッドと同様に、メソッドを持つインターフェースへの不適切なデコード試行でパニックの原因となっていました。
    *   **変更後**: `if v.Kind() == reflect.Interface && v.NumMethod() == 0 { ... }` に条件が変更されました。
        これにより、デコード先のインターフェースが `interface{}` (空インターフェース) である場合にのみ、最適化された `d.objectInterface()` パスが使用され、JSONオブジェクトは `map[string]interface{}` にデコードされます。メソッドを持つインターフェース型へのデコード試行は、この `if` 文の条件を満たさないため、後続の `default` ケース(変更後のコードでは省略されていますが、`UnmarshalTypeError` を返すロジックが適用される)で適切にエラー処理されます。

3.  **`func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool)`**:
    この関数は、JSONの真偽値、文字列、数値といったリテラル値をGoの型にデコードします。それぞれのケースで `reflect.Interface` 型へのデコードロジックが修正されています。
    *   **真偽値 (`case reflect.Bool`)**:
        ```go
        case reflect.Interface:
            if v.NumMethod() == 0 {
                v.Set(reflect.ValueOf(value))
            } else {
                d.saveError(&UnmarshalTypeError{"bool", v.Type()})
            }
        ```
        `v.NumMethod() == 0` の条件が追加され、空インターフェースの場合のみ真偽値をセットします。それ以外(メソッドを持つインターフェース)の場合は `UnmarshalTypeError` を発生させます。
    *   **文字列 (`case reflect.String`)**:
        ```go
        case reflect.Interface:
            if v.NumMethod() == 0 {
                v.Set(reflect.ValueOf(string(s)))
            } else {
                d.saveError(&UnmarshalTypeError{"string", v.Type()})
            }
        ```
        同様に `v.NumMethod() == 0` の条件が追加され、空インターフェースの場合のみ文字列をセットします。それ以外の場合は `UnmarshalTypeError` を発生させます。
    *   **数値 (`default` - 数値の処理部分)**:
        ```go
        if v.NumMethod() != 0 {
            d.saveError(&UnmarshalTypeError{"number", v.Type()})
            break
        }
        v.Set(reflect.ValueOf(n))
        ```
        数値のデコード処理において、`v.NumMethod() != 0` の条件が追加されました。これにより、メソッドを持つインターフェース型に数値をデコードしようとした場合、`UnmarshalTypeError` が発生し、パニックが回避されます。

### `src/pkg/encoding/json/decode_test.go`

*   **`decodeTypeErrorTests` 変数**:
    様々な型ミスマッチのシナリオを網羅するテストデータが追加されました。特に、`new(string)` にJSONオブジェクトをデコードするケース(Issue 4628)と、`new(error)` に様々なJSONデータ型をデコードするケース(Issue 4222)が含まれています。
*   **`TestUnmarshalTypeError` 関数**:
    `decodeTypeErrorTests` の各項目について `json.Unmarshal` を実行し、返されたエラーが期待通り `*UnmarshalTypeError` であることを検証しています。これにより、以前パニックを引き起こしていた状況が、適切にエラーとして処理されるようになったことが確認されます。

これらの変更により、`encoding/json` パッケージは、デコード先のGoの型とJSONデータの型がミスマッチした場合に、より予測可能で堅牢なエラーハンドリングを提供するようになりました。

## 関連リンク

*   GitHubコミットページ: [https://github.com/golang/go/commit/406ca3c2f19a0742a05e5837ca9cae77fb4cadd8](https://github.com/golang/go/commit/406ca3c2f19a0742a05e5837ca9cae77fb4cadd8)
*   Go Issue 4222: `json.Unmarshal` panics when unmarshaling into `error` type (現在は閉鎖)
*   Go Issue 4628: `json.Unmarshal` panics when unmarshaling into `*string` with `{"user": "name"}` (現在は閉鎖)
*   Go CL 7100049: [https://golang.org/cl/7100049](https://golang.org/cl/7100049) (Go Code Review)

## 参考にした情報源リンク

*   Go言語公式ドキュメント: `encoding/json` パッケージ: [https://pkg.go.dev/encoding/json](https://pkg.go.dev/encoding/json)
*   Go言語公式ドキュメント: `reflect` パッケージ: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
*   Go言語のインターフェースについて: [https://go.dev/tour/methods/10](https://go.dev/tour/methods/10)
*   Go言語の `interface{}` について: [https://go.dev/blog/effective-go#interfaces](https://go.dev/blog/effective-go#interfaces)
*   Go言語の `error` インターフェースについて: [https://go.dev/blog/error-handling-and-go](https://go.dev/blog/error-handling-and-go)
*   Go言語の `reflect.Value.NumMethod()` の挙動について (一般的なリフレクションの解説): [https://pkg.go.dev/reflect#Value.NumMethod](https://pkg.go.dev/reflect#Value.NumMethod)