[インデックス 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) encoderFunc
vx
(例となる値) を受け取っていたため、そのvx
のアドレス可能性に依存したエンコーダを生成する可能性がありました。 - 変更後:
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc
vx
引数が削除され、代わりに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言語のインターフェースとポインタに関する一般的な知識