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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、バイトスライス([]byte)型のJSONエンコード/デコードに関する以前の変更(CL 11161044 / ba455262a9db)を元に戻すものです。具体的には、json.Unmarshal が名前付きバイトスライス型(例: type MyBytes []byte)を文字列からデコードできるようにする変更を取り消し、関連するテストコードとドキュメントの修正も元に戻しています。

コミット

commit 4419d7e53cba0d897c5962af4ad1dd0b4aaf0b21
Author: Russ Cox <rsc@golang.org>
Date:   Fri Jul 12 17:42:01 2013 -0400

    undo CL 11161044 / ba455262a9db
    
    I want to think more carefully about this.
    
    We put this in because Marshal encoded named []byte but Unmarshal rejected them.
    And we noticed that Marshal's behavior was undocumented so we documented it.
    But I am starting to think the docs and Unmarshal were correct and Marshal's
    behavior was the problem.
    
    Rolling back to give us more time to think.
    
    ««« original CL description
    json: unmarshal types that are byte slices.
    
    The json package cheerfully would marshal
    
            type S struct {
                    IP net.IP
            }
    
    but would give an error when unmarshalling.  This change allows any
    type whose concrete type is a byte slice to be unmarshalled from a
    string.
    
    Fixes #5086.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/11161044
    
    »»»
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/11042046

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

https://github.com/golang/go/commit/4419d7e53cba0d897c5962af4ad1dd0b4aaf0b21

元コミット内容

このコミットが元に戻した元のコミット(CL 11161044 / ba455262a9db)の目的は、encoding/json パッケージにおける json.Marshaljson.Unmarshal の間の非一貫性を解消することでした。

元のコミットの意図は以下の通りです。

  • 問題点: json.Marshal は、net.IP のように []byte を基底型とするカスタム型をJSON文字列(Base64エンコードされた形式)として正しくマーシャルできました。しかし、json.Unmarshal は、そのような型をJSON文字列からデコードしようとするとエラーを返していました。これは、json.Marshal[]byte 型を特別扱いしてBase64文字列に変換する一方で、json.Unmarshal がその逆の操作を、基底型が []byte であるカスタム型に対して行えなかったためです。
  • 解決策: 元のコミットは、この非対称性を修正するために、json.Unmarshal が「具象型がバイトスライスである任意の型」をJSON文字列からアンマーシャルできるように変更しました。これにより、net.IP のような型も、JSON文字列として正しくデコードできるようになるはずでした。
  • 関連するIssue: この変更は、GitHub Issue #5086 を修正することを目的としていました。

変更の背景

このコミットの背景には、Goの encoding/json パッケージにおける []byte 型の扱いに関する設計上の再考があります。

元のコミット(CL 11161044)は、json.Marshal[]byte 型(およびそのエイリアスや構造体フィールドとしてのカスタム型)をBase64エンコードされた文字列として扱う一方で、json.Unmarshal がその逆の操作を、名前付きの []byte 型に対して行えないという非対称性を修正しようとしました。この非対称性は、特に net.IP のような標準ライブラリの型が []byte を基底としている場合に問題となりました。net.IPjson.Marshal で文字列に変換できるのに、json.Unmarshal で文字列から net.IP に戻せないという状況は、ユーザーにとって直感的ではありませんでした。

元のコミットでは、この問題を解決するために json.Unmarshal のロジックを拡張し、[]byte を基底型とする任意の型を文字列からデコードできるようにしました。また、json.Marshal のこの挙動がドキュメント化されていなかったため、ドキュメントも更新されました。

しかし、このコミット(CL 11042046)の作者であるRuss Coxは、この変更を元に戻すことを決定しました。その理由は、json.Marshal が名前付きの []byte 型をBase64エンコードされた文字列として扱うこと自体が、もしかしたら問題なのではないか、という疑問が生じたためです。つまり、Unmarshal の挙動やドキュメントが正しく、Marshal の挙動が問題だったのではないか、という再評価が行われたのです。

このコミットは、この設計上の疑問を解決するために、一度元の状態に戻し、より慎重に検討する時間を持つことを目的としています。これは、Go言語の標準ライブラリが、一貫性と直感的な挙動を重視して設計されていることを示しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびJSONに関する前提知識が必要です。

  1. Go言語の encoding/json パッケージ:

    • Goの標準ライブラリの一部で、Goの構造体とJSONデータの間でエンコード(マーシャル)およびデコード(アンマーシャル)を行う機能を提供します。
    • json.Marshal(v interface{}) ([]byte, error): Goの値をJSON形式のバイトスライスに変換します。
    • json.Unmarshal(data []byte, v interface{}) error: JSON形式のバイトスライスをGoの値に変換します。
    • 型とJSONのマッピング: encoding/json は、Goの様々な型をJSONのプリミティブ型にマッピングします。
      • string -> JSON文字列
      • int, float64 -> JSON数値
      • bool -> JSONブーリアン
      • struct -> JSONオブジェクト
      • array, slice -> JSON配列
      • nil -> JSON null
    • []byte の特殊な扱い: encoding/json パッケージでは、[]byte 型はJSON配列ではなく、Base64エンコードされたJSON文字列として扱われるという特別なルールがあります。これは、バイナリデータをJSONに埋め込む際の一般的な慣習に合わせたものです。
  2. Go言語の型システムと reflect パッケージ:

    • 基底型 (Underlying Type): Goでは、type MyBytes []byte のように既存の型から新しい型を宣言できます。この場合、MyBytes の基底型は []byte です。MyBytes[]byte とは異なる型ですが、同じ基底型を持ちます。
    • reflect パッケージ: Goのランタイムリフレクション機能を提供します。これにより、プログラムの実行中に変数の型や値を検査・操作できます。
      • reflect.TypeOf(v interface{}) reflect.Type: 値の動的な型情報を取得します。
      • reflect.Value.Kind() reflect.Kind: 型のカテゴリ(例: reflect.Slice, reflect.Struct, reflect.Int など)を取得します。
      • reflect.Type.Elem() reflect.Type: ポインタ、配列、スライス、マップの要素型を取得します。例えば、[]byteElem()byte (または uint8) を返します。
  3. Base64エンコーディング:

    • バイナリデータをASCII文字列形式に変換するエンコーディング方式です。JSONはテキストベースのフォーマットであるため、バイナリデータを直接埋め込むことはできません。そのため、[]byte のようなバイナリデータは、JSONに含める際にBase64エンコードされて文字列として表現されます。
  4. GoのCL (Change List) とIssue:

    • CL: Goプロジェクトでは、コード変更は「Change List (CL)」として提出され、レビューされます。https://golang.org/cl/ のURLでアクセスできます。
    • Issue: バグ報告や機能要望は、GoプロジェクトのIssueトラッカー(GitHubのIssueや以前のGoのIssueトラッカー)で管理されます。Fixes #XXXX は、そのコミットが特定のIssueを修正することを示す慣習です。

これらの知識があることで、[]byte の特殊な扱い、reflect パッケージを用いた型チェック、そしてGoプロジェクトの変更管理プロセスが、このコミットの意図と影響を理解する上で重要であることがわかります。

技術的詳細

このコミットは、encoding/json パッケージにおける json.Unmarshal の挙動を元に戻すことで、名前付きバイトスライス型(例: type MyBytes []byte)のデコードに関する以前の変更を撤回しています。

元のコミット(CL 11161044)では、json.Unmarshal が文字列から値をデコードする際に、ターゲットのGoの型が []byte そのものではなく、その基底型が []byte であるカスタム型(例: net.IP)であっても、Base64デコードを試みるように変更されていました。これは、json.Marshal がそのようなカスタム型をBase64文字列としてマーシャルする挙動と一貫性を持たせるためでした。

このコミットが行った具体的な変更は以下の通りです。

  1. src/pkg/encoding/json/decode.go の変更:

    • decodeState 構造体の literalStore メソッド内で、文字列をスライス型にデコードするロジックが変更されました。
    • 元のコミットでは、if v.Type().Elem().Kind() != reflect.Uint8 { というチェックが if v.Type() != byteSliceType { に変更されていました。
    • このコミットは、この変更を元に戻し、if v.Type().Elem().Kind() != reflect.Uint8 { に戻しました。
    • 意味:
      • v.Type().Elem().Kind() != reflect.Uint8: これは、スライスの要素の型が uint8 (つまり byte) でない場合にエラーとするチェックです。これは、[]byte 型だけでなく、type MyBytes []byte のようなカスタム型も Elem().Kind()reflect.Uint8 を返すため、これらを文字列からデコードしようとするとエラーになります。
      • v.Type() != byteSliceType: これは、スライスの型が厳密に []byte 型であるかどうかをチェックします。byteSliceTypereflect.TypeOf([]byte(nil)) で取得される []byte のリフレクト型です。このチェックでは、type MyBytes []byte のようなカスタム型は byteSliceType とは異なるため、文字列からのデコードが許可されることになります(元のコミットの意図)。
    • このコミットで元に戻されたことにより、json.Unmarshal は、[]byte そのものでない限り、基底型が []byte であっても文字列からのデコードを拒否する挙動に戻りました。
  2. src/pkg/encoding/json/decode_test.go の変更:

    • TestByteSliceType というテスト関数が完全に削除されました。
    • このテストは、元のコミットで追加されたもので、type A []byte のような名前付きバイトスライス型が json.Marshaljson.Unmarshal の両方で正しく機能することを確認するためのものでした。
    • 元のコミットの変更が元に戻されたため、このテストも不要となり削除されました。
  3. src/pkg/encoding/json/encode.go の変更:

    • json.Marshal のドキュメントコメントが変更されました。
    • 元のコミットでは、「バイトスライスのスライスはBase64エンコードされた文字列としてエンコードされる」という記述が、「[]byte はBase64エンコードされた文字列としてエンコードされる」という記述に修正されていました。
    • このコミットは、このドキュメントの変更を元に戻し、元の「バイトスライスのスライス」という表現に戻しました。
    • 意味: このドキュメントの変更は、json.Marshal[]byte そのものだけでなく、type MyBytes []byte のようなカスタム型もBase64エンコードされた文字列として扱うという挙動を反映しようとしたものです。しかし、このコミットで元に戻されたことで、ドキュメントはより一般的な表現に戻り、json.Marshal の挙動に関する再考の余地を残しています。

これらの変更は、encoding/json パッケージにおける []byte 型の扱い、特にカスタム型が絡む場合の挙動について、より深い設計上の議論が必要であるというRuss Coxの判断に基づいています。json.Marshaljson.Unmarshal の一貫性をどのように保つか、そして []byte の特殊な扱いをどこまで拡張するかという点が、このコミットの核心にある技術的な課題です。

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

このコミットにおけるコアとなるコードの変更箇所は以下の3つのファイルにわたります。

  1. src/pkg/encoding/json/decode.go

    --- a/src/pkg/encoding/json/decode.go
    +++ b/src/pkg/encoding/json/decode.go
    @@ -660,7 +660,7 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool
     		default:
     			d.saveError(&UnmarshalTypeError{"string", v.Type()})
     		case reflect.Slice:
    -			if v.Type().Elem().Kind() != reflect.Uint8 {
    +			if v.Type() != byteSliceType {
     				d.saveError(&UnmarshalTypeError{"string", v.Type()})
     				break
     			}
    

    変更内容: if v.Type() != byteSliceType {if v.Type().Elem().Kind() != reflect.Uint8 { に戻されました。これは、文字列からスライスへのデコードを許可する条件を、より厳密な []byte 型に限定する(元のコミットの意図を元に戻す)変更です。

  2. src/pkg/encoding/json/decode_test.go

    --- a/src/pkg/encoding/json/decode_test.go
    +++ b/src/pkg/encoding/json/decode_test.go
    @@ -1186,32 +1186,3 @@ func TestSkipArrayObjects(t *testing.T) {
     		t.Errorf("got error %q, want nil", err)
     	}
     }
    -
    -// Test that types of byte slices (such as net.IP) both
    -// marshal and unmarshal.
    -func TestByteSliceType(t *testing.T) {
    -	type A []byte
    -	type S struct {
    -		A A
    -	}
    -
    -	for x, in := range []S{
    -		S{},
    -		S{A: []byte{'1'}},
    -		S{A: []byte{'1', '2', '3', '4', '5'}},
    -	} {
    -		data, err := Marshal(&in)
    -		if err != nil {
    -			t.Errorf("#%d: got Marshal error %q, want nil", x, err)
    -			continue
    -		}
    -		var out S
    -		err = Unmarshal(data, &out)
    -		if err != nil {
    -			t.Fatalf("#%d: got Unmarshal error %q, want nil", x, err)
    -		}
    -		if !reflect.DeepEqual(&out, &in) {
    -			t.Fatalf("#%d: got %v, want %v", x, &out, &in)
    -		}
    -	}
    -}
    

    変更内容: TestByteSliceType テスト関数が完全に削除されました。このテストは、元のコミットで追加された、名前付きバイトスライス型のマーシャル/アンマーシャルを検証するものでした。

  3. src/pkg/encoding/json/encode.go

    --- a/src/pkg/encoding/json/encode.go
    +++ b/src/pkg/encoding/json/encode.go
    @@ -44,8 +44,8 @@ import (
     // The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e"
     // to keep some browsers from misinterpreting JSON output as HTML.
     //
    -// Array and slice values encode as JSON arrays, except that a slice of
    -// bytes encodes as a base64-encoded string, and a nil slice
    +// Array and slice values encode as JSON arrays, except that
    +// []byte encodes as a base64-encoded string, and a nil slice
     // encodes as the null JSON object.
     //
     // Struct values encode as JSON objects. Each exported struct field
    

    変更内容: コメントが // Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice から // Array and slice values encode as JSON arrays, except that a slice of // bytes encodes as a base64-encoded string, and a nil slice に戻されました。これは、[]byte の特殊な扱いに関するドキュメントの記述を、より一般的な表現に戻す変更です。

コアとなるコードの解説

このコミットの核心は、encoding/json パッケージが []byte 型をどのように扱うか、特にカスタム型が絡む場合の挙動を再評価することにあります。

  1. src/pkg/encoding/json/decode.go の変更 (literalStore メソッド):

    • literalStore メソッドは、JSONのプリミティブ値(文字列、数値、ブーリアンなど)をGoの対応する型にデコードする役割を担っています。
    • 変更された行は、JSON文字列をGoのスライス型にデコードしようとする際の型チェックです。
    • 元のコミットの変更 (if v.Type().Elem().Kind() != reflect.Uint8 { から if v.Type() != byteSliceType {) の意図:
      • v.Type().Elem().Kind() != reflect.Uint8 は、スライスの要素が uint8 (つまり byte) でない場合にエラーを発生させます。これは、[]int のようなスライスが文字列からデコードされるのを防ぎます。しかし、type MyBytes []byte のようなカスタム型も Elem().Kind()reflect.Uint8 を返すため、このチェックでは MyBytes を文字列からデコードしようとするとエラーになります。
      • v.Type() != byteSliceType は、スライスの型が厳密に []byte 型であるかどうかをチェックします。byteSliceTypereflect.TypeOf([]byte(nil)) で取得される []byte のリフレクト型です。このチェックでは、MyBytes のようなカスタム型は byteSliceType とは異なるため、文字列からのデコードが許可されることになります。これが元のコミットの目的でした。つまり、net.IP のような []byte を基底とするカスタム型も文字列からデコードできるようにする、という意図です。
    • このコミットでのロールバック (if v.Type() != byteSliceType { から if v.Type().Elem().Kind() != reflect.Uint8 {) の意味:
      • このロールバックにより、json.Unmarshal は、文字列をスライスにデコードする際に、そのスライスの要素が uint8 でない場合にエラーを発生させるという、より一般的な(そして元の)挙動に戻りました。
      • 結果として、type MyBytes []byte のようなカスタム型は、[]byte そのものでない限り、JSON文字列からデコードされる際に UnmarshalTypeError を発生させることになります。これは、json.Marshal がこれらのカスタム型をBase64文字列としてエンコードする挙動との非対称性を再導入します。
  2. src/pkg/encoding/json/decode_test.goTestByteSliceType 削除:

    • このテストは、元のコミットで追加されたもので、type A []byte のような名前付きバイトスライス型が json.Marshaljson.Unmarshal の両方で正しく機能することを確認するためのものでした。
    • decode.go の変更が元に戻されたため、このテストはもはや期待される挙動をテストしないため、削除されました。これは、元のコミットの機能が撤回されたことを明確に示しています。
  3. src/pkg/encoding/json/encode.go のコメント変更:

    • このコメントは、json.Marshal[]byte 型をBase64エンコードされた文字列として扱うという特別なルールを説明しています。
    • 元のコミットでは、この説明が「[]byte」に限定されていましたが、このコミットで「バイトスライスのスライス」というより一般的な表現に戻されました。
    • この変更は、json.Marshal[]byte そのものだけでなく、type MyBytes []byte のようなカスタム型もBase64エンコードされた文字列として扱うという、実際の挙動を反映しようとしたものです。しかし、このコミットで元に戻されたことで、ドキュメントはより一般的な表現に戻り、json.Marshal の挙動に関する再考の余地を残しています。

全体として、このコミットは、encoding/json パッケージにおける []byte 型の扱い、特にカスタム型が絡む場合の設計上の複雑さと、MarshalUnmarshal の一貫性をどのように実現するかという課題を浮き彫りにしています。Russ Coxは、Marshal の既存の挙動自体が問題である可能性を指摘し、より慎重な検討のために以前の変更を一時的に元に戻すことを選択しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのコミット履歴
  • Go言語のIssueトラッカー
  • Base64エンコーディングに関する一般的な情報