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

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

このコミットは、Go言語の標準ライブラリである encoding/json パッケージにおける、nil スライスのJSONマーシャリング(Goのデータ構造からJSONへの変換)とアンマーシャリング(JSONからGoのデータ構造への変換)の挙動を修正するものです。具体的には、nil スライスがJSONでは null として表現され、JSONの null がGoの nil スライスとして正しくパースされるように変更されました。これにより、Goの nil スライスと空のスライス [] のJSON表現が区別され、より直感的で期待される挙動が実現されました。

コミット

  • コミットハッシュ: 48c75c5f9c8e97b87fbd8f24dffa73d6b2148691
  • Author: Alexander Reece awreece@gmail.com
  • Date: Mon Oct 31 13:59:23 2011 -0400

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

https://github.com/golang/go/commit/48c75c5f9c8e97b87fbd8f24dffa73d6b2148691

元コミット内容

json: Properly handle nil slices.

Marshal nil slices as null and parse null value as a nil slice.
Fixes #2278.

R=rsc
CC=golang-dev
https://golang.org/cl/5257053

変更の背景

このコミットは、Goの encoding/json パッケージが nil スライスをJSONにマーシャリングする際に、空のJSON配列 [] として扱ってしまうという問題(Issue #2278)を修正するために行われました。また、JSONの null 値をGoの nil スライスとして正しくアンマーシャリングすることも目的としています。

Goにおいて、スライスは nil である状態と、要素を持たない空の状態 [] が区別されます。しかし、この修正以前は、json.Marshalnil スライスを null ではなく [] として出力していました。これは、JSONの仕様や他の言語の挙動と異なる場合があり、特にデータベースのNULL許容フィールドなどと連携する際に問題となる可能性がありました。

例えば、Goの構造体で []string 型のフィールドが nil の場合、JSON出力が [] となると、受信側が「空のリスト」と解釈してしまい、「値が存在しない(null)」という意図が伝わらないという不整合が生じていました。この修正により、Goの nil スライスはJSONの null に、JSONの null はGoの nil スライスに、それぞれ正しくマッピングされるようになります。

前提知識の解説

Goにおけるスライスとnil

Go言語において、スライスは配列のセグメントを記述するデータ構造です。スライスは内部的にポインタ、長さ、容量の3つの要素を持ちます。

  • nil スライス: スライスのゼロ値は nil です。var s []int のように宣言されたスライスは nil であり、そのポインタは nil、長さは0、容量も0です。nil スライスは有効なスライスであり、長さが0であるため、多くの操作(len(s)cap(s)for range s など)で空のスライスと同様に振る舞います。
  • 空のスライス: make([]int, 0)[]int{} のように初期化されたスライスは空のスライスです。これらのスライスは nil ではありませんが、長さは0です。ポインタは有効なメモリを指しますが、そのメモリには要素がありません。

JSONマーシャリングの文脈では、nil スライスと空のスライスがJSONでどのように表現されるべきかという点が重要になります。一般的に、JSONでは「値が存在しない」ことを null で表現し、「空のリスト」を [] で表現します。この区別は、APIの設計やデータの一貫性において非常に重要です。

JSONマーシャリングとアンマーシャリング

  • マーシャリング (Marshaling): Goのデータ構造(構造体、スライス、マップなど)をJSON形式のバイト列に変換するプロセスです。json.Marshal() 関数がこれを行います。
  • アンマーシャリング (Unmarshaling): JSON形式のバイト列をGoのデータ構造に変換するプロセスです。json.Unmarshal() 関数がこれを行います。

encoding/json パッケージは、Goのデータ型とJSONのデータ型の間で自動的な変換を行います。例えば、Goの string はJSONの文字列に、Goの int はJSONの数値に、Goの map や構造体はJSONのオブジェクトに、Goのスライスや配列はJSONの配列に変換されます。

このコミットの焦点は、Goの nil スライスがJSONの null に、そしてJSONの null がGoの nil スライスに、それぞれ正しくマッピングされるようにすることです。

技術的詳細

このコミットでは、src/pkg/json/decode.gosrc/pkg/json/encode.go の2つの主要なファイルが変更され、対応するテストファイル src/pkg/json/decode_test.gosrc/pkg/json/encode_test.go も更新されています。

json.Marshal の変更 (encode.go)

以前の json.Marshal は、reflect.Slice 型の値を処理する際に、nil であるかどうかを区別せず、常に空のJSON配列 [] として出力していました。 このコミットでは、encode.goreflect.Slice を処理する casev.IsNil() のチェックが追加されました。

// src/pkg/json/encode.go
case reflect.Slice:
    if v.IsNil() {
        e.WriteString("null")
        break
    }
    // Slices can be marshalled as nil, but otherwise are handled
    // as arrays.
    fallthrough
case reflect.Array:
    // ... (既存の配列処理ロジック)

この変更により、reflect.Slice の値が nil である場合、e.WriteString("null") が呼び出され、JSON出力が "null" となります。nil でない場合は fallthrough を使用して reflect.Array の処理ロジックにフォールスルーし、通常の配列としてマーシャリングされます。これにより、Goの nil スライスはJSONの null として正しく表現されるようになりました。

json.Unmarshal の変更 (decode.go)

json.Unmarshal は、JSONの null 値をGoのデータ構造にデコードする際に、reflect.Slice 型のフィールドに対して nil を設定できるように変更されました。

// src/pkg/json/decode.go
func (d *decodeState) literalStore(item []byte, v reflect.Value) {
    // ...
    case 'n': // null
        // ...
        switch v.Kind() {
        default:
            d.saveError(&UnmarshalTypeError{"null", v.Type()})
        case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: // ここに reflect.Slice が追加
            v.Set(reflect.Zero(v.Type()))
        }
    // ...
}

literalStore 関数は、JSONの null リテラルをGoの対応する型にデコードする役割を担っています。以前は reflect.Slicenull を受け入れる型として明示的にリストされていませんでした。この変更により、reflect.Slicecase reflect.Interface, reflect.Ptr, reflect.Map と同じように null を受け入れ、v.Set(reflect.Zero(v.Type())) を呼び出すことで、そのスライスを nil に設定できるようになりました。reflect.Zero(v.Type()) は、指定された型のゼロ値を返します。スライス型の場合、そのゼロ値は nil スライスです。

テストの変更 (decode_test.go, encode_test.go)

テストファイルでは、nil スライスが null としてマーシャリングされ、nullnil スライスとしてアンマーシャリングされることを検証するためのテストケースが追加・修正されています。

decode_test.go では、allValueIndent および pallValueIndent 変数内の NilSliceEmptySlice の期待されるJSON表現が [] から null に変更されています。これにより、JSONの null がGoの nil スライスとして正しくデコードされることを確認します。

encode_test.go では、optionalsExpected 変数内の slr (slice) の期待されるJSON表現が [] から null に変更されています。これにより、Goの nil スライスがJSONの null として正しくエンコードされることを確認します。

これらの変更により、Goの encoding/json パッケージは、nil スライスと空のスライスのJSON表現をより厳密に区別し、JSONの null とGoの nil スライスの間のマッピングを正確に行うようになりました。

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

src/pkg/json/decode.go

--- a/src/pkg/json/decode.go
+++ b/src/pkg/json/decode.go
@@ -588,7 +588,7 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value) {
 		switch v.Kind() {
 		default:
 			d.saveError(&UnmarshalTypeError{"null", v.Type()})
-		case reflect.Interface, reflect.Ptr, reflect.Map:
+		case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
 			v.Set(reflect.Zero(v.Type()))
 		}

src/pkg/json/decode_test.go

--- a/src/pkg/json/decode_test.go
+++ b/src/pkg/json/decode_test.go
@@ -456,7 +456,7 @@ var allValueIndent = `{\
 	"PSlice": null,
 	"PSliceP": null,
 	"EmptySlice": [],
-	"NilSlice": [],
+	"NilSlice": null,
 	"StringSlice": [
 		"str24",
 		"str25",
@@ -528,8 +528,8 @@ var pallValueIndent = `{\
 	},
 	"EmptyMap": null,
 	"NilMap": null,
-	"Slice": [],
-	"SliceP": [],
+	"Slice": null,
+	"SliceP": null,
 	"PSlice": [
 		{
 			"Tag": "tag20"
@@ -547,10 +547,10 @@ var pallValueIndent = `{\
 			"Tag": "tag23"
 		}
 	],
-	"EmptySlice": [],
-	"NilSlice": [],
-	"StringSlice": [],
-	"ByteSlice": "",
+	"EmptySlice": null,
+	"NilSlice": null,
+	"StringSlice": null,
+	"ByteSlice": null,
 	"Small": {
 		"Tag": ""
 	},

src/pkg/json/encode.go

--- a/src/pkg/json/encode.go
+++ b/src/pkg/json/encode.go
@@ -352,7 +352,15 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
 		}
 		e.WriteByte('}')

-	case reflect.Array, reflect.Slice:
+	case reflect.Slice:
+		if v.IsNil() {
+			e.WriteString("null")
+			break
+		}
+		// Slices can be marshalled as nil, but otherwise are handled
+		// as arrays.
+		fallthrough
+	case reflect.Array:
 		if v.Type() == byteSliceType {
 			e.WriteByte('"')
 			s := v.Interface().([]byte)

src/pkg/json/encode_test.go

--- a/src/pkg/json/encode_test.go
+++ b/src/pkg/json/encode_test.go
@@ -28,7 +28,7 @@ type Optionals struct {
 var optionalsExpected = `{\
  "sr": "",
  "omitempty": 0,\
- "slr": [],
+ "slr": null,\
  "mr": {}\
 }`

コアとなるコードの解説

src/pkg/json/decode.go の変更

literalStore 関数は、JSONの null リテラルをGoの対応する型にデコードする際に呼び出されます。 変更前は、reflect.Interface, reflect.Ptr, reflect.Map のみが null を受け入れて reflect.Zero(v.Type()) でゼロ値(nil)に設定されていました。 変更後、reflect.Slice がこのリストに追加されました。これにより、JSONの null がGoのスライス型にデコードされる際に、そのスライスが nil に設定されるようになりました。これは、nil スライスがGoにおけるスライスのゼロ値であるため、自然な挙動です。

src/pkg/json/encode.go の変更

reflectValueQuoted 関数は、Goの reflect.Value をJSONにエンコードする主要なロジックを含んでいます。 変更前は、reflect.Arrayreflect.Slice が同じ case で処理されており、nil スライスと空のスライスが区別されずに、常に空のJSON配列 [] としてエンコードされていました。 変更後、reflect.Slice が独立した case として分離されました。この新しい reflect.Slicecase 内で、v.IsNil() を使ってスライスが nil であるかどうかがチェックされます。

  • もし v.IsNil()true であれば、e.WriteString("null") を呼び出してJSON出力に "null" を書き込み、break で処理を終了します。
  • もし v.IsNil()false であれば、fallthrough を使用して reflect.Arraycase に処理を移します。これにより、nil でないスライス(空のスライス [] や要素を持つスライス)は、既存の配列エンコードロジックによってJSON配列としてエンコードされます。

この変更により、Goの nil スライスはJSONの null として、Goの空のスライス [] はJSONの [] として、それぞれ明確に区別されてエンコードされるようになりました。

テストファイルの変更 (decode_test.go, encode_test.go)

テストファイルの変更は、上記のコード変更が意図した通りに動作することを確認するためのものです。

  • decode_test.go では、JSON文字列内の NilSliceEmptySlice の期待値が [] から null に変更されています。これは、JSONの null がGoの nil スライスとして正しくアンマーシャリングされることを検証します。
  • encode_test.go では、Goの構造体をJSONにマーシャリングした際の期待値として、nil スライスに対応するJSONが null となるように修正されています。

これらのテストの修正は、新しい nil スライスの処理ロジックが正しく実装され、既存の動作に悪影響を与えていないことを保証します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (encoding/json パッケージ)
  • Go言語のスライスに関する公式ドキュメント
  • JSONの仕様 (RFC 7159)
  • Go言語のIssueトラッカー (GitHub)
  • Go言語のコードレビューシステム (Gerrit/CL)

参考にした情報源リンク


注記: 上記のWeb検索結果は、このコミットが修正したIssue #2278に関する一般的な議論や、Goの encoding/json パッケージにおける nil スライスの挙動に関する情報を提供しています。このコミット自体は2011年のものであり、Web検索結果の一部はそれ以降の議論や、nil スライスと空のスライスの区別に関するGoの設計思想について言及している可能性があります。