[インデックス 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.Marshal
と json.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.IP
は json.Marshal
で文字列に変換できるのに、json.Unmarshal
で文字列から net.IP
に戻せないという状況は、ユーザーにとって直感的ではありませんでした。
元のコミットでは、この問題を解決するために json.Unmarshal
のロジックを拡張し、[]byte
を基底型とする任意の型を文字列からデコードできるようにしました。また、json.Marshal
のこの挙動がドキュメント化されていなかったため、ドキュメントも更新されました。
しかし、このコミット(CL 11042046)の作者であるRuss Coxは、この変更を元に戻すことを決定しました。その理由は、json.Marshal
が名前付きの []byte
型をBase64エンコードされた文字列として扱うこと自体が、もしかしたら問題なのではないか、という疑問が生じたためです。つまり、Unmarshal
の挙動やドキュメントが正しく、Marshal
の挙動が問題だったのではないか、という再評価が行われたのです。
このコミットは、この設計上の疑問を解決するために、一度元の状態に戻し、より慎重に検討する時間を持つことを目的としています。これは、Go言語の標準ライブラリが、一貫性と直感的な挙動を重視して設計されていることを示しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびJSONに関する前提知識が必要です。
-
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
-> JSONnull
[]byte
の特殊な扱い:encoding/json
パッケージでは、[]byte
型はJSON配列ではなく、Base64エンコードされたJSON文字列として扱われるという特別なルールがあります。これは、バイナリデータをJSONに埋め込む際の一般的な慣習に合わせたものです。
-
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
: ポインタ、配列、スライス、マップの要素型を取得します。例えば、[]byte
のElem()
はbyte
(またはuint8
) を返します。
- 基底型 (Underlying Type): Goでは、
-
Base64エンコーディング:
- バイナリデータをASCII文字列形式に変換するエンコーディング方式です。JSONはテキストベースのフォーマットであるため、バイナリデータを直接埋め込むことはできません。そのため、
[]byte
のようなバイナリデータは、JSONに含める際にBase64エンコードされて文字列として表現されます。
- バイナリデータをASCII文字列形式に変換するエンコーディング方式です。JSONはテキストベースのフォーマットであるため、バイナリデータを直接埋め込むことはできません。そのため、
-
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を修正することを示す慣習です。
- CL: Goプロジェクトでは、コード変更は「Change List (CL)」として提出され、レビューされます。
これらの知識があることで、[]byte
の特殊な扱い、reflect
パッケージを用いた型チェック、そしてGoプロジェクトの変更管理プロセスが、このコミットの意図と影響を理解する上で重要であることがわかります。
技術的詳細
このコミットは、encoding/json
パッケージにおける json.Unmarshal
の挙動を元に戻すことで、名前付きバイトスライス型(例: type MyBytes []byte
)のデコードに関する以前の変更を撤回しています。
元のコミット(CL 11161044)では、json.Unmarshal
が文字列から値をデコードする際に、ターゲットのGoの型が []byte
そのものではなく、その基底型が []byte
であるカスタム型(例: net.IP
)であっても、Base64デコードを試みるように変更されていました。これは、json.Marshal
がそのようなカスタム型をBase64文字列としてマーシャルする挙動と一貫性を持たせるためでした。
このコミットが行った具体的な変更は以下の通りです。
-
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
型であるかどうかをチェックします。byteSliceType
はreflect.TypeOf([]byte(nil))
で取得される[]byte
のリフレクト型です。このチェックでは、type MyBytes []byte
のようなカスタム型はbyteSliceType
とは異なるため、文字列からのデコードが許可されることになります(元のコミットの意図)。
- このコミットで元に戻されたことにより、
json.Unmarshal
は、[]byte
そのものでない限り、基底型が[]byte
であっても文字列からのデコードを拒否する挙動に戻りました。
-
src/pkg/encoding/json/decode_test.go
の変更:TestByteSliceType
というテスト関数が完全に削除されました。- このテストは、元のコミットで追加されたもので、
type A []byte
のような名前付きバイトスライス型がjson.Marshal
とjson.Unmarshal
の両方で正しく機能することを確認するためのものでした。 - 元のコミットの変更が元に戻されたため、このテストも不要となり削除されました。
-
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.Marshal
と json.Unmarshal
の一貫性をどのように保つか、そして []byte
の特殊な扱いをどこまで拡張するかという点が、このコミットの核心にある技術的な課題です。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の3つのファイルにわたります。
-
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
型に限定する(元のコミットの意図を元に戻す)変更です。 -
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
テスト関数が完全に削除されました。このテストは、元のコミットで追加された、名前付きバイトスライス型のマーシャル/アンマーシャルを検証するものでした。 -
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
型をどのように扱うか、特にカスタム型が絡む場合の挙動を再評価することにあります。
-
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
型であるかどうかをチェックします。byteSliceType
はreflect.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文字列としてエンコードする挙動との非対称性を再導入します。
- このロールバックにより、
-
src/pkg/encoding/json/decode_test.go
のTestByteSliceType
削除:- このテストは、元のコミットで追加されたもので、
type A []byte
のような名前付きバイトスライス型がjson.Marshal
とjson.Unmarshal
の両方で正しく機能することを確認するためのものでした。 decode.go
の変更が元に戻されたため、このテストはもはや期待される挙動をテストしないため、削除されました。これは、元のコミットの機能が撤回されたことを明確に示しています。
- このテストは、元のコミットで追加されたもので、
-
src/pkg/encoding/json/encode.go
のコメント変更:- このコメントは、
json.Marshal
が[]byte
型をBase64エンコードされた文字列として扱うという特別なルールを説明しています。 - 元のコミットでは、この説明が「
[]byte
」に限定されていましたが、このコミットで「バイトスライスのスライス」というより一般的な表現に戻されました。 - この変更は、
json.Marshal
が[]byte
そのものだけでなく、type MyBytes []byte
のようなカスタム型もBase64エンコードされた文字列として扱うという、実際の挙動を反映しようとしたものです。しかし、このコミットで元に戻されたことで、ドキュメントはより一般的な表現に戻り、json.Marshal
の挙動に関する再考の余地を残しています。
- このコメントは、
全体として、このコミットは、encoding/json
パッケージにおける []byte
型の扱い、特にカスタム型が絡む場合の設計上の複雑さと、Marshal
と Unmarshal
の一貫性をどのように実現するかという課題を浮き彫りにしています。Russ Coxは、Marshal
の既存の挙動自体が問題である可能性を指摘し、より慎重な検討のために以前の変更を一時的に元に戻すことを選択しました。
関連リンク
- 元のコミット (CL 11161044): https://golang.org/cl/11161044
- 関連するIssue (#5086): https://github.com/golang/go/issues/5086
- Go言語
encoding/json
パッケージのドキュメント: https://pkg.go.dev/encoding/json - Go言語
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect
参考にした情報源リンク
- Go言語の公式ドキュメント
- GitHubのGoリポジトリのコミット履歴
- Go言語のIssueトラッカー
- Base64エンコーディングに関する一般的な情報