[インデックス 17690] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける、JSONエンコーダの内部的な挙動に関するバグ修正です。具体的には、json.Marshal が値のアドレス可能性(reflect.Value.CanAddr())を誤ってキャッシュし、その結果、実行時にパニックを引き起こす可能性があった問題を解決します。エンコーダの決定を、型のアドレス可能性ではなく、実行時の値のアドレス可能性に基づいて動的に行うように変更されました。
コミット
commit 0f3ea75020cf7dda64805fe9aeef26be60cf16cd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Sep 23 19:57:19 2013 -0700
encoding/json: don't cache value addressability when building first encoder
newTypeEncoder (called once per type and then cached) was
looking at the first value seen of that type's addressability
and caching the encoder decision. If the first value seen was
addressable and a future one wasn't, it would panic.
Instead, introduce a new wrapper encoder type that checks
CanAddr at runtime.
Fixes #6458
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/13839045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0f3ea75020cf7dda64805fe9aeef26be60cf16cd
元コミット内容
encoding/json パッケージにおいて、最初のエンコーダ構築時に値のアドレス可能性をキャッシュしないようにする変更です。
newTypeEncoder 関数は、型ごとに一度だけ呼び出され、その結果がキャッシュされます。この関数は、その型で最初に見られた値のアドレス可能性を調べてエンコーダの決定をキャッシュしていました。もし最初に見られた値がアドレス可能であったにもかかわらず、後続の値がアドレス可能でなかった場合、パニックが発生していました。
この問題を解決するため、実行時に CanAddr をチェックする新しいラッパーエンコーダ型が導入されました。
この変更は Issue #6458 を修正します。
変更の背景
Go言語の encoding/json パッケージは、Goの値をJSON形式にエンコードする機能を提供します。このパッケージは効率性を高めるために、一度エンコードされた型のエンコーダをキャッシュする仕組みを持っています。具体的には、newTypeEncoder 関数が特定の型に対するエンコーダを生成し、これが typeEncoder によってキャッシュされます。
問題は、newTypeEncoder がエンコーダを生成する際に、その型で「最初に見られた値」のアドレス可能性(reflect.Value.CanAddr())に基づいてエンコーダの挙動を決定し、その決定をキャッシュしていた点にありました。
Goの reflect パッケージでは、reflect.Value が表す値がアドレス可能であるかどうかを CanAddr() メソッドで確認できます。アドレス可能であるとは、その値がメモリ上の特定のアドレスを持ち、ポインタを取得できることを意味します。例えば、構造体のフィールドやスライスの要素は通常アドレス可能ですが、マップの値やインターフェースの値は直接的にはアドレス可能ではありません。
json.Marshaler や encoding.TextMarshaler といったインターフェースを実装する型は、JSONエンコード時に特別な処理を必要とします。これらのインターフェースは、値そのものが実装することもあれば、その値へのポインタが実装することもあります。例えば、type MyType struct {} が MyType 自身ではなく *MyType が json.Marshaler を実装している場合、MyType のインスタンスをエンコードする際には、そのインスタンスのアドレスを取得してポインタとして json.Marshaler メソッドを呼び出す必要があります。
従来の newTypeEncoder は、エンコーダを生成する際に、引数として渡された vx reflect.Value (その型の「例となる値」) の CanAddr() の結果を見て、アドレスが必要な Marshaler 実装が存在するかどうかを判断していました。もし vx がアドレス可能で、かつ vx.Addr().Interface().(Marshaler) が成功した場合、アドレスを必要とするエンコーダ(addrMarshalerEncoder など)を生成してキャッシュしていました。
しかし、この「例となる値」は、必ずしもその型でエンコードされるすべての値のアドレス可能性を代表するものではありませんでした。例えば、json.Marshal(&myStruct) のようにポインタを渡した場合、myStruct はアドレス可能ですが、json.Marshal(myStruct) のように値を直接渡した場合、myStruct はアドレス可能ではない(コピーされた値が渡されるため)可能性があります。
もし、最初に json.Marshal(&myStruct) が呼び出され、myStruct がアドレス可能であると判断されて addrMarshalerEncoder がキャッシュされたとします。その後、json.Marshal(myStruct) のようにアドレス可能ではない myStruct のコピーが渡された場合、キャッシュされた addrMarshalerEncoder は v.CanAddr() が false である値に対して v.Addr() を呼び出そうとし、Goのランタイムパニック(panic: reflect.Value.Addr of unaddressable value)を引き起こしていました。
このコミットは、このアドレス可能性の誤ったキャッシュが引き起こすパニックを防ぐために行われました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と encoding/json パッケージの内部動作に関する知識が必要です。
-
reflectパッケージ:reflect.Type: Goの型の静的な情報(名前、種類、メソッドなど)を表します。reflect.TypeOf(v)で取得できます。reflect.Value: Goの実行時の値の動的な情報(実際の値、アドレス可能性、フィールド、メソッドなど)を表します。reflect.ValueOf(v)で取得できます。reflect.Value.CanAddr():reflect.Valueが表す値がアドレス可能であるかどうかを返します。trueの場合、Addr()メソッドを呼び出してその値へのポインタを取得できます。reflect.Value.Addr():reflect.Valueが表す値へのポインタをreflect.Valueとして返します。CanAddr()がfalseの場合に呼び出すとパニックします。reflect.Value.Elem(): ポインタが指す要素のreflect.Valueを返します。reflect.Type.Implements(u reflect.Type):reflect.Typeがインターフェースuを実装しているかどうかを返します。
-
encoding/jsonパッケージのエンコーダキャッシュ:encoding/jsonパッケージは、パフォーマンス最適化のために、Goの型ごとにJSONエンコーダをキャッシュします。これにより、同じ型の値を複数回エンコードする際に、エンコーダの生成コストを削減できます。typeEncoder(t reflect.Type)関数が、特定のreflect.Typeに対応するencoderFuncを取得します。もしキャッシュに存在しない場合、newTypeEncoderを呼び出して新しいエンコーダを生成し、キャッシュに格納します。encoderFuncはfunc(e *encodeState, v reflect.Value, quoted bool)というシグネチャを持つ関数で、実際のエンコード処理を行います。
-
json.Marshalerインターフェース:type Marshaler interface { MarshalJSON() ([]byte, error) }このインターフェースを実装する型は、
MarshalJSONメソッドを自分で定義することで、JSONへのエンコード方法をカスタマイズできます。 -
encoding.TextMarshalerインターフェース:type TextMarshaler interface { MarshalText() (text []byte, err error) }このインターフェースを実装する型は、
MarshalTextメソッドを自分で定義することで、テキスト形式(JSON文字列、数値、真偽値など)へのエンコード方法をカスタマイズできます。 -
アドレスを必要とするインターフェース実装: Goでは、インターフェースは値型でもポインタ型でも実装できます。
type MyType struct {}がfunc (m MyType) MarshalJSON() ([]byte, error)を持つ場合、MyTypeはjson.Marshalerを実装します。type MyType struct {}がfunc (m *MyType) MarshalJSON() ([]byte, error)を持つ場合、*MyTypeはjson.Marshalerを実装します。この場合、MyTypeの値をエンコードするには、その値へのポインタを取得する必要があります。
技術的詳細
このコミットの核心は、エンコーダの生成時に値のアドレス可能性を静的に判断してキャッシュするのではなく、実行時に値のアドレス可能性を動的にチェックするメカニズムを導入した点にあります。
主な変更点は以下の通りです。
-
newTypeEncoderのシグネチャ変更:- 変更前:
func newTypeEncoder(t reflect.Type, vx reflect.Value) encoderFuncvx(例となる値) を受け取っていたため、そのvxのアドレス可能性に依存したエンコーダを生成する可能性がありました。 - 変更後:
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFuncvx引数が削除され、代わりにallowAddrというブール値が導入されました。これは、このエンコーダがアドレス可能な値も処理できるべきかどうかを示すフラグです。これにより、エンコーダ生成時に特定の値のアドレス可能性に依存するのをやめ、型の特性と、そのエンコーダがアドレスを扱う必要があるかどうかの一般的なフラグに基づいてエンコーダを生成するようになりました。
- 変更前:
-
condAddrEncoder型の導入:- 新しい構造体
condAddrEncoderが導入されました。
type condAddrEncoder struct { canAddrEnc, elseEnc encoderFunc }- この構造体は、2つの
encoderFuncを持ちます。canAddrEnc: 値がアドレス可能 (v.CanAddr() == true) な場合に呼び出されるエンコーダ。elseEnc: 値がアドレス可能でない (v.CanAddr() == false) 場合に呼び出されるエンコーダ。
condAddrEncoderのencodeメソッドは、エンコード対象のreflect.ValueのCanAddr()を実行時にチェックし、その結果に基づいて適切なエンコーダ(canAddrEncまたはelseEnc)に処理を委譲します。
- 新しい構造体
-
newCondAddrEncoder関数の導入:func newCondAddrEncoder(canAddrEnc, elseEnc encoderFunc) encoderFunc- この関数は、
condAddrEncoderのインスタンスを生成し、そのencodeメソッドをencoderFuncとして返します。これにより、アドレス可能性のチェックと適切なエンコーダの選択がカプセル化されます。
-
MarshalerおよびTextMarshalerの処理ロジックの変更:newTypeEncoder内でMarshalerやTextMarshalerインターフェースの実装をチェックする際、t.Implements(marshalerType)(値がインターフェースを実装しているか) とreflect.PtrTo(t).Implements(marshalerType)(ポインタがインターフェースを実装しているか) の両方を考慮するようになりました。- もしポインタがインターフェースを実装している場合(例:
*MyTypeがjson.Marshalerを実装)、newCondAddrEncoderを使用して、実行時に値がアドレス可能かどうかをチェックし、アドレス可能であればaddrMarshalerEncoder(またはaddrTextMarshalerEncoder) を、そうでなければnewTypeEncoder(t, false)で生成されたエンコーダ(アドレスを必要としないエンコーダ)を呼び出すようにしました。これにより、同じ型でも、値として渡された場合とポインタとして渡された場合の両方に対応できるようになります。
-
newStructEncoder,newMapEncoder,newSliceEncoder,newArrayEncoder,newPtrEncoderの変更:- これらの関数も
newTypeEncoderと同様に、vx reflect.Value引数を削除し、reflect.Typeのみを受け取るように変更されました。これにより、エンコーダ生成時に具体的な値のアドレス可能性に依存するのを完全に排除しました。 newStructEncoderでは、フィールドのエンコーダを生成する際にtypeByIndex(t, f.index)を使用して、フィールドの型情報のみに基づいてエンコーダを取得するように変更されました。
- これらの関数も
-
typeByIndexヘルパー関数の追加:func typeByIndex(t reflect.Type, index []int) reflect.Type- このヘルパー関数は、構造体のフィールドのインデックスパスに基づいて、そのフィールドの
reflect.Typeを取得します。ポインタ型の場合も適切にElem()を呼び出して処理します。
これらの変更により、encoding/json は、エンコーダをキャッシュする際に、値のアドレス可能性という動的な特性を考慮に入れる必要がなくなり、エンコード時に初めてその値のアドレス可能性をチェックするようになりました。これにより、異なるアドレス可能性を持つ同じ型の値が混在しても、パニックを起こすことなく正しくエンコードできるようになりました。
コアとなるコードの変更箇所
src/pkg/encoding/json/encode.go:encoderFuncのシグネチャ変更:_ boolからquoted boolへ。valueEncoder関数からv reflect.ValueのType()を直接取得するように変更。typeEncoder関数のシグネチャ変更:vx reflect.Value引数を削除。newTypeEncoder関数のシグネチャ変更:vx reflect.Value引数を削除し、allowAddr bool引数を追加。newTypeEncoder内でのMarshalerおよびTextMarshalerのチェックロジックの変更とnewCondAddrEncoderの使用。newTypeEncoder内のswitch vx.Kind()をswitch t.Kind()に変更。newStructEncoder,newMapEncoder,newSliceEncoder,newArrayEncoder,newPtrEncoderのシグネチャ変更:vx reflect.Value引数を削除。newStructEncoder内でフィールドエンコーダを取得するロジックの変更。condAddrEncoder構造体とnewCondAddrEncoder関数の追加。typeByIndexヘルパー関数の追加。
src/pkg/encoding/json/encode_test.go:TestIssue6458という新しいテストケースの追加。
コアとなるコードの解説
このコミットの主要な変更は、encoding/json パッケージがエンコーダを生成し、キャッシュする方法の根本的な見直しにあります。
-
typeEncoder(t reflect.Type): この関数は、特定のreflect.Typeに対応するencoderFuncを取得する役割を担います。変更前はtypeEncoder(t reflect.Type, vx reflect.Value)でしたが、vx(例となる値) が削除され、型情報のみに基づいてエンコーダを取得するようになりました。これにより、キャッシュキーが型のみになり、値のアドレス可能性に依存しなくなりました。 -
newTypeEncoder(t reflect.Type, allowAddr bool): この関数は、実際に型tのエンコーダを構築します。allowAddr引数は、このエンコーダがアドレス可能な値も処理できるべきかどうかを示します。これは、json.Marshal(&x)のようにポインタが渡された場合に、そのポインタが指す値がアドレス可能であるかどうかを考慮する必要があることを示唆します。Marshaler/TextMarshalerの処理: 最も重要な変更点の一つです。以前はvx.Interface().(Marshaler)やvx.Addr().Interface().(Marshaler)のように、具体的な値vxを使ってインターフェース実装をチェックしていました。 変更後は、t.Implements(marshalerType)(値が実装) とreflect.PtrTo(t).Implements(marshalerType)(ポインタが実装) の両方をチェックします。 もし*TがMarshalerを実装している場合(つまり、値Tをエンコードするにはアドレスが必要な場合)、newCondAddrEncoderを使用してエンコーダを生成します。
ここでif t.Kind() != reflect.Ptr && allowAddr { // 値型で、かつアドレスを考慮する必要がある場合 if reflect.PtrTo(t).Implements(marshalerType) { // ポインタがMarshalerを実装している場合 return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false)) } }newCondAddrEncoderが使われることで、エンコード時にv.CanAddr()をチェックし、アドレス可能であればaddrMarshalerEncoder(ポインタ経由でMarshalJSONを呼び出すエンコーダ) を、そうでなければnewTypeEncoder(t, false)で生成されたエンコーダ(アドレスを必要としないエンコーダ)を呼び出すように動的に切り替わります。これにより、同じ型でも、アドレス可能かどうかに応じて適切なエンコードパスが選択されるようになります。
-
condAddrEncoderとnewCondAddrEncoder: これらは、実行時のアドレス可能性チェックをカプセル化するための新しいメカニズムです。condAddrEncoderは、canAddrEnc(アドレス可能な場合のエンコーダ) とelseEnc(アドレス可能でない場合のエンコーダ) の2つのencoderFuncを保持します。 そのencodeメソッドは、渡されたreflect.Value vに対してv.CanAddr()を呼び出し、結果に応じてcanAddrEncまたはelseEncを実行します。 これにより、エンコーダがキャッシュされる時点ではアドレス可能性を固定せず、実際のエンコード時に動的に判断できるようになりました。 -
newStructEncoderなどの変更:newStructEncoderや他の複合型(マップ、スライス、配列、ポインタ)のエンコーダ生成関数も、vx reflect.Value引数を削除し、reflect.Typeのみを受け取るようになりました。これにより、エンコーダの生成が型情報のみに依存するようになり、特定のインスタンスのアドレス可能性に影響されることがなくなりました。 特にnewStructEncoderでは、フィールドのエンコーダを生成する際にse.fieldEncs[i] = typeEncoder(typeByIndex(t, f.index))となり、フィールドの型情報に基づいてエンコーダを取得するようになりました。 -
typeByIndex: 構造体のネストされたフィールドの型をreflect.Typeから取得するためのヘルパー関数です。reflect.Valueを使わずに型情報のみでフィールドの型を辿れるようにするために導入されました。
これらの変更により、encoding/json は、エンコーダのキャッシュが値のアドレス可能性に起因するパニックを引き起こすことなく、より堅牢に動作するようになりました。
関連リンク
- Go言語
encoding/jsonパッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go言語
reflectパッケージ公式ドキュメント: https://pkg.go.dev/reflect - Go言語
encodingパッケージ公式ドキュメント: https://pkg.go.dev/encoding
参考にした情報源リンク
- Go言語の公式ドキュメント (
encoding/json,reflectパッケージ) - コミットメッセージとコードの差分
- Go言語のインターフェースとポインタに関する一般的な知識