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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおけるJSONデコード時の挙動変更に関するものです。具体的には、JSONのキーがGoの構造体のエクスポートされていない(unexported)フィールド名と一致した場合の処理が修正されています。Go 1.0ではこの場合に UnmarshalFieldError が発生していましたが、この変更によりエラーを発生させずに該当フィールドをスキップするようになります。

コミット

commit 6e3f3af4e0f4293f868aa71c4271cab6344d5797
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date:   Tue Jan 22 17:49:07 2013 -0500

    encoding/json: ignore unexported fields in Unmarshal
    
    Go 1.0 behavior was to create an UnmarshalFieldError when a json value name matched an unexported field name. This error will no longer be created and the field will be skipped instead.
    
    Fixes #4660.
    
    R=adg, rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/7139049

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

https://github.com/golang/go/commit/6e3f3af4e0f4293f868aa71c4271cab6344d5797

元コミット内容

Go 1.0では、encoding/json パッケージの Unmarshal 関数がJSON値をGoの構造体にデコードする際、JSONのキーが構造体のエクスポートされていないフィールド(小文字で始まるフィールド)と一致した場合に UnmarshalFieldError を発生させていました。このコミットは、この挙動を変更し、エラーを発生させる代わりに、該当するエクスポートされていないフィールドを単にスキップするようにします。これにより、JSONデコード時のエラーハンドリングがより柔軟になります。

この変更は、GoのIssue #4660を修正するものです。ただし、現在のGoのIssueトラッカーで #4660 を検索すると別のIssueが表示されるため、このコミットが参照しているのは古い、既にクローズされたIssueである可能性が高いです。

変更の背景

Go言語の設計思想の一つに「明示的なエラーハンドリング」がありますが、同時に「不要なエラーは発生させない」という考え方もあります。encoding/json パッケージは、Goの構造体のエクスポートされたフィールド(大文字で始まるフィールド)のみをJSONとの間でマッピングするという明確なルールを持っています。エクスポートされていないフィールドは、パッケージ外部からはアクセスできないため、JSONデコードの対象外とすることが自然な挙動です。

Go 1.0の時点では、JSONキーとエクスポートされていないフィールド名が一致した場合にエラーを発生させることで、開発者にその事実を通知していました。しかし、これは場合によっては過剰なエラーであり、開発者が意図的にエクスポートされていないフィールドをJSONデコードの対象外としたい場合に、不必要なエラーハンドリングを強制することになります。

この変更の背景には、encoding/jsonUnmarshal の挙動をよりGoの慣習に合わせ、エクスポートされていないフィールドは「無視されるべきもの」として扱うという方針転換があったと考えられます。これにより、開発者はJSONデータにエクスポートされていないフィールド名と一致するキーが含まれていても、それが意図しないエラーとして扱われることを心配する必要がなくなります。

前提知識の解説

Go言語におけるエクスポートされたフィールドとエクスポートされていないフィールド

Go言語では、識別子(変数名、関数名、型名、構造体のフィールド名など)の最初の文字が大文字か小文字かによって、その識別子が「エクスポートされている(exported)」か「エクスポートされていない(unexported)」かが決まります。

  • エクスポートされた識別子(Exported Identifiers): 最初の文字が大文字の識別子です。これらは、その識別子が定義されているパッケージの外部からアクセス可能です。例えば、json.Marshalfmt.PrintlnMarshalPrintln はエクスポートされた関数です。構造体のフィールドの場合、エクスポートされたフィールドは他のパッケージから直接アクセスしたり、JSONエンコード/デコードの対象になったりします。
  • エクスポートされていない識別子(Unexported Identifiers): 最初の文字が小文字の識別子です。これらは、その識別子が定義されているパッケージ内からのみアクセス可能です。パッケージの外部からは直接アクセスできません。構造体のフィールドの場合、エクスポートされていないフィールドは、その構造体が定義されているパッケージ内でのみ利用され、通常、encoding/json のような外部パッケージからは無視されます。

encoding/json パッケージの Unmarshal

encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。

  • json.Marshal: Goのデータ構造(通常は構造体)をJSON形式のバイトスライスにエンコード(マーシャル)します。この際、エクスポートされたフィールドのみがJSONに出力されます。
  • json.Unmarshal: JSON形式のバイトスライスをGoのデータ構造にデコード(アンマーシャル)します。この際、JSONのキーとGoの構造体のエクスポートされたフィールド名が一致した場合に、そのフィールドに値が設定されます。

Goの encoding/json パッケージは、デフォルトでエクスポートされたフィールドのみを処理するという設計思想に基づいています。これは、Goのアクセス制御メカニズムと一貫しており、外部に公開すべきでない内部的なデータはJSONシリアライズ/デシリアライズの対象外とすることで、カプセル化を促進します。

UnmarshalFieldError (Go 1.0での挙動)

Go 1.0の encoding/json パッケージでは、Unmarshal 処理中にJSONのキーが構造体のエクスポートされていないフィールド名と一致した場合、UnmarshalFieldError というエラーを返していました。このエラーは、開発者に対して「JSONデータに、Goの構造体では処理できない(エクスポートされていない)フィールドが含まれている」ことを明示的に通知する目的がありました。

しかし、この挙動は、JSONデータがGoの構造体と完全に一致しない場合(例えば、JSONデータがより多くのフィールドを持っている場合や、Goの構造体がJSONデータの一部しか必要としない場合)に、不必要なエラーを引き起こす可能性がありました。多くのケースでは、エクスポートされていないフィールドに対応するJSONキーは単に無視されることが期待されます。

技術的詳細

このコミットの技術的な変更点は、encoding/json パッケージの decode.go ファイル内の object メソッドにおける、エクスポートされていないフィールドに対するエラーチェックロジックの削除です。

Goの encoding/json パッケージは、リフレクション(reflect パッケージ)を使用して、Goの構造体とJSONデータの間のマッピングを行います。Unmarshal 関数がJSONオブジェクトをGoの構造体にデコードする際、内部的には decodeState 構造体の object メソッドが呼び出されます。このメソッドは、JSONオブジェクトの各キーと値のペアを反復処理し、対応するGoの構造体フィールドを探します。

変更前のコードでは、JSONキーに対応するエクスポートされたフィールドが見つからなかった場合、さらに構造体のエクスポートされていないフィールドをスキャンし、JSONキーと名前が一致するエクスポートされていないフィールドがあれば UnmarshalFieldError を生成していました。このスキャンは、f.PkgPath != "" という条件でエクスポートされていないフィールドを識別し、strings.EqualFold(f.Name, key) で大文字小文字を区別せずに名前の一致を確認していました。

このコミットでは、このエラーチェックロジック全体が削除されました。これにより、JSONキーがエクスポートされていないフィールド名と一致しても、エラーは発生せず、単にそのキーと値のペアは無視されるようになります。

この変更は、encoding/jsonUnmarshal の挙動を、エクスポートされていないフィールドは「無視されるべきもの」というGoの慣習に完全に合わせるものです。これにより、JSONデータがGoの構造体よりも多くのフィールドを持っていても、それらがエクスポートされていないフィールドに対応する限り、エラーなしでデコードが成功するようになります。

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

src/pkg/encoding/json/decode.go

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -106,6 +106,7 @@ func (e *UnmarshalTypeError) Error() string {
 
 // An UnmarshalFieldError describes a JSON object key that
 // led to an unexported (and therefore unwritable) struct field.
+// (No longer used; kept for compatibility.)
 type UnmarshalFieldError struct {
 	Key   string
 	Type  reflect.Type
@@ -530,15 +531,6 @@ func (d *decodeState) object(v reflect.Value) {
 					subv = subv.Field(i)
 				}
 			} else {
-				// To give a good error, a quick scan for unexported fields in top level.
-				st := v.Type()
-				for i := 0; i < st.NumField(); i++ {
-					f := st.Field(i)
-					if f.PkgPath != "" && strings.EqualFold(f.Name, key) {
-						d.saveError(&UnmarshalFieldError{key, st, f})
-					}
-				}
 			}
 		}
 

src/pkg/encoding/json/decode_test.go

--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -199,7 +199,7 @@ var unmarshalTests = []unmarshalTest{
 	{in: `\"invalid: \\uD834x\\uDD1E\"`, ptr: new(string), out: \"invalid: \\uFFFDx\\uFFFD\"},\n \t{in: \"null\", ptr: new(interface{}), out: nil},\n \t{in: `{\"X\": [1,2,3], \"Y\": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{\"array\", reflect.TypeOf(\"\")}},\n-\t{in: `{\"x\": 1}`, ptr: new(tx), out: tx{}, err: &UnmarshalFieldError{\"x\", txType, txType.Field(0)}},\n+\t{in: `{\"x\": 1}`, ptr: new(tx), out: tx{}},\n \t{in: `{\"F1\":1,\"F2\":2,\"F3\":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number(\"3\")}},\n \t{in: `{\"F1\":1,\"F2\":2,\"F3\":3}`, ptr: new(V), out: V{F1: Number(\"1\"), F2: int32(2), F3: Number(\"3\")}, useNumber: true},\n \t{in: `{\"k1\":1,\"k2\":\"s\",\"k3\":[1,2.0,3e-3],\"k4\":{\"kk1\":\"s\",\"kk2\":2}}`, ptr: new(interface{}), out: ifaceNumAsFloat64},\n@@ -1064,3 +1064,25 @@ func TestUnmarshalTypeError(t *testing.T) {\n \t\t}\n \t}\n }\n+\n+// Test handling of unexported fields that should be ignored.\n+// Issue 4660\n+type unexportedFields struct {\n+\tName string\n+\tm    map[string]interface{} `json:\"-\"`\n+\tm2   map[string]interface{} `json:\"abcd\"`\n+}\n+\n+func TestUnmarshalUnexported(t *testing.T) {\n+\tinput := `{\"Name\": \"Bob\", \"m\": {\"x\": 123}, \"m2\": {\"y\": 456}, \"abcd\": {\"z\": 789}}`\n+\twant := &unexportedFields{Name: \"Bob\"}\n+\n+\tout := &unexportedFields{}\n+\terr := Unmarshal([]byte(input), out)\n+\tif err != nil {\n+\t\tt.Errorf(\"got error %v, expected nil\", err)\n+\t}\n+\tif !reflect.DeepEqual(out, want) {\n+\t\tt.Errorf(\"got %q, want %q\", out, want)\n+\t}\n+}\n```

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

### `src/pkg/encoding/json/decode.go` の変更

`UnmarshalFieldError` の定義には、コメント `// (No longer used; kept for compatibility.)` が追加され、このエラー型がもはや内部的に使用されないことが明示されています。これは、既存のコードがこのエラー型を参照している場合に互換性を保つための措置です。

最も重要な変更は、`decodeState` 構造体の `object` メソッド内で行われています。

```go
			} else {
				// To give a good error, a quick scan for unexported fields in top level.
				st := v.Type()
				for i := 0; i < st.NumField(); i++ {
					f := st.Field(i)
					if f.PkgPath != "" && strings.EqualFold(f.Name, key) {
						d.saveError(&UnmarshalFieldError{key, st, f})
					}
				}
			}

上記の else ブロック全体が削除されています。このブロックは、JSONキーに対応するエクスポートされたフィールドが見つからなかった場合に、エクスポートされていないフィールドを探索し、一致するものがあれば UnmarshalFieldError を生成する役割を担っていました。このコードが削除されたことで、JSONキーがエクスポートされていないフィールド名と一致しても、エラーは発生せず、そのキーと値のペアは単に無視されるようになります。

src/pkg/encoding/json/decode_test.go の変更

テストファイル decode_test.go では、この変更を反映するために2つの修正が行われています。

  1. unmarshalTests スライス内の既存のテストケースの修正: {in: {"x": 1}, ptr: new(tx), out: tx{}, err: &UnmarshalFieldError{\"x\", txType, txType.Field(0)}}, この行は、tx という構造体のエクスポートされていないフィールド x にJSON値をデコードしようとした際に UnmarshalFieldError が発生することを期待するテストケースでした。変更後、このエラーは発生しなくなるため、テストケースは以下のように修正されています。 {in: {"x": 1}, ptr: new(tx), out: tx{}}, err フィールドが削除され、エラーが発生しないことを期待するようになりました。

  2. 新しいテストケース TestUnmarshalUnexported の追加: この新しいテスト関数は、エクスポートされていないフィールドが正しく無視されることを検証します。 unexportedFields という構造体が定義されており、mm2 という2つのエクスポートされていないフィールドが含まれています。m には json:"-" タグが付いており、JSONエンコード/デコードから完全に除外されることを示します。m2 には json:"abcd" タグが付いていますが、これはエクスポートされていないフィールドであるため、JSONデコード時には無視されるべきです。 テスト入力 input には、"m""abcd" というキーが含まれています。 期待される出力 want は、Name フィールドのみがデコードされ、mm2 は初期値のままであることを示しています。 Unmarshal を実行した後、エラーが発生しないこと、そしてデコードされた構造体が期待される値と一致すること(つまり、エクスポートされていないフィールドが無視されたこと)が reflect.DeepEqual を使って検証されています。

これらの変更により、encoding/json パッケージは、JSONキーがGoの構造体のエクスポートされていないフィールド名と一致した場合に、エラーを発生させることなくそのフィールドをスキップする、より柔軟でGoの慣習に沿った挙動を示すようになります。

関連リンク

参考にした情報源リンク

  • Go言語におけるエクスポートされたフィールドとエクスポートされていないフィールドに関する解説記事 (一般的なGoのドキュメントやチュートリアル)
  • encoding/json の挙動に関するStack Overflowの議論など
  • GoのIssueトラッカー (Issue #4660の具体的な内容については、古いIssueのため直接参照できませんでしたが、一般的なGoのIssueの検索方法を参考にしました)