[インデックス 11571] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおける、json.Marshaler
インターフェースの実装に関するバグ修正です。具体的には、ポインタレシーバを持つ MarshalJSON
メソッドが、アドレス可能な(つまりメモリ上のアドレスを持つ)非ポインタ型の値に対して正しく呼び出されない問題を解決します。
コミット
commit bf89d58e738a492012ee67af0ab57b0a322dea0b
Author: David Symonds <dsymonds@golang.org>
Date: Fri Feb 3 11:15:06 2012 +1100
encoding/json: call (*T).MarshalJSON for addressable T values.
Fixes #2170.
R=golang-dev, cw, adg
CC=golang-dev
https://golang.org/cl/5618045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bf89d58e738a492012ee67af0ab57b0a322dea0b
元コミット内容
encoding/json: call (*T).MarshalJSON for addressable T values.
Fixes #2170.
変更の背景
このコミットは、Go言語の encoding/json
パッケージが抱えていた、特定の条件下で json.Marshaler
インターフェースが正しく機能しないバグ(Issue #2170)を修正するために行われました。
問題の核心は、MarshalJSON
メソッドがポインタレシーバ (func (t *MyType) MarshalJSON()
) を持つ場合にありました。Goの encoding/json
パッケージは、構造体のフィールドなど、アドレスを持つがポインタ型ではない値に対して MarshalJSON
を呼び出す際に、その値が json.Marshaler
インターフェースを満たしているかを正しく判定できていませんでした。
具体的には、json.Marshaler
インターフェースは MarshalJSON() ([]byte, error)
メソッドを要求します。ある型 T
がこのインターフェースをポインタレシーバで実装している場合、*T
型の変数のみが直接このインターフェースを満たします。しかし、T
型の変数(ポインタではないがアドレスを持つ)に対しても、Goの言語仕様上は &T
としてポインタを取得し、そのポインタを通じて MarshalJSON
を呼び出すことが可能です。encoding/json
パッケージは、この「アドレス可能な非ポインタ型」の場合に、MarshalJSON
メソッドの存在を見落としてしまうことがありました。
これにより、開発者がカスタムのJSONマーシャリングロジックを MarshalJSON
メソッドで定義しても、その型が構造体のフィールドとして埋め込まれていたり、値として渡されたりすると、期待通りに MarshalJSON
が呼び出されず、デフォルトのJSONマーシャリング(フィールド名をキーとするなど)が適用されてしまうという予期せぬ挙動が発生していました。
前提知識の解説
Go言語の encoding/json
パッケージ
encoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換(マーシャリングとアンマーシャリング)を行うための標準ライブラリです。Goの構造体をJSONに変換する際には、通常、構造体のフィールド名がJSONのキーとなり、フィールドの値がJSONの値となります。
json.Marshaler
インターフェース
encoding/json
パッケージは、カスタムのJSONマーシャリングロジックを定義するための json.Marshaler
インターフェースを提供しています。このインターフェースは以下のシグネチャを持つ単一のメソッドを定義しています。
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
ある型がこの MarshalJSON
メソッドを実装すると、json.Marshal
関数はその型の値をJSONに変換する際に、デフォルトのマーシャリングロジックの代わりに、このカスタムメソッドを呼び出します。
Go言語のレシーバ(値レシーバ vs ポインタレシーバ)とインターフェース
Go言語のメソッドは、レシーバの型によって「値レシーバ」と「ポインタレシーバ」の2種類があります。
-
値レシーバ (
func (t MyType) MethodName()
):- メソッドが呼び出される際、レシーバの値のコピーがメソッドに渡されます。
- メソッド内でレシーバの値を変更しても、元の値には影響しません。
MyType
型の変数も*MyType
型の変数も、値レシーバを持つメソッドを呼び出すことができます。Goは必要に応じて自動的に値のコピーを作成したり、ポインタをデリファレンスしたりします。- インターフェースの観点では、型
MyType
が値レシーバでインターフェースを実装している場合、MyType
と*MyType
の両方がそのインターフェースを満たします。
-
ポインタレシーバ (
func (t *MyType) MethodName()
):- メソッドが呼び出される際、レシーバのポインタのコピーがメソッドに渡されます。
- メソッド内でレシーバの値を変更すると、元の値にも影響します。
*MyType
型の変数のみが直接ポインタレシーバを持つメソッドを呼び出すことができます。MyType
型の変数からポインタレシーバを持つメソッドを呼び出すには、その変数が「アドレス可能」である必要があります。Goはアドレス可能な値に対しては自動的にアドレスを取得してメソッドを呼び出します。- インターフェースの観点では、型
MyType
がポインタレシーバでインターフェースを実装している場合、*MyType
のみがそのインターフェースを満たします。MyType
は通常、そのインターフェースを満たしません。
アドレス可能性 (Addressability)
Goにおいて「アドレス可能」とは、その値がメモリ上の特定のアドレスに存在し、そのアドレスを取得できる(&
演算子を適用できる)ことを意味します。
例えば、変数、構造体のフィールド、配列の要素などはアドレス可能です。しかし、関数の戻り値やマップの要素(マップは値のコピーを返すため)などはアドレス可能ではありません。
このコミットの文脈では、T
型の構造体フィールドはアドレス可能ですが、それ自体はポインタ型ではありません。encoding/json
は、このような「アドレス可能な非ポインタ型」に対して、ポインタレシーバを持つ MarshalJSON
メソッドを正しく検出できていませんでした。
技術的詳細
encoding/json
パッケージの encode.go
ファイルには、Goの値をJSONにエンコードするロジックが含まれています。このコミット以前のバージョンでは、reflect.Value
を使って値が json.Marshaler
インターフェースを満たしているかをチェックしていました。
元のコードは以下のようになっていました(簡略化)。
if j, ok := v.Interface().(Marshaler); ok && (v.Kind() != reflect.Ptr || !v.IsNil()) {
// ... MarshalJSON を呼び出す ...
}
ここで v
は reflect.Value
型の変数で、エンコード対象のGoの値を表します。
v.Interface().(Marshaler)
は、v
が表す値が直接 Marshaler
インターフェースを満たしているかをチェックします。
問題は、MyType
がポインタレシーバ (func (t *MyType) MarshalJSON()
) で Marshaler
を実装している場合、MyType
型の変数(例えば struct { F MyType }
の F
)は、それ自体は Marshaler
インターフェースを満たしません。*MyType
型のポインタのみが Marshaler
インターフェースを満たします。
しかし、F
はアドレス可能なので、&F
とすることで *MyType
型のポインタを取得でき、そのポインタを通じて MarshalJSON
を呼び出すことが可能です。元のコードは、この「アドレス可能な非ポインタ型」の場合に、v.Interface().(Marshaler)
が false
を返すため、ポインタレシーバを持つ MarshalJSON
メソッドを見落としていました。
このコミットは、この見落としを修正し、v
が直接 Marshaler
を満たさない場合でも、v
がアドレス可能であれば、そのアドレス (v.Addr()
) が Marshaler
を満たすかどうかをチェックするように変更しました。これにより、ポインタレシーバを持つ MarshalJSON
メソッドが、アドレス可能な非ポインタ型の値に対しても正しく呼び出されるようになります。
コアとなるコードの変更箇所
変更は主に src/pkg/encoding/json/encode.go
ファイルの reflectValueQuoted
関数にあります。
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -262,8 +262,18 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
return
}
- if j, ok := v.Interface().(Marshaler); ok && (v.Kind() != reflect.Ptr || !v.IsNil()) {
- b, err := j.MarshalJSON()
+ m, ok := v.Interface().(Marshaler)
+ if !ok {
+ // T doesn't match the interface. Check against *T too.
+ if v.Kind() != reflect.Ptr && v.CanAddr() {
+ m, ok = v.Addr().Interface().(Marshaler)
+ if ok {
+ v = v.Addr()
+ }
+ }
+ }
+ if ok && (v.Kind() != reflect.Ptr || !v.IsNil()) {
+ b, err := m.MarshalJSON()
if err == nil {
// copy JSON into buffer, checking validity.
err = Compact(&e.Buffer, b)
また、この変更を検証するためのテストケースが src/pkg/encoding/json/decode_test.go
と src/pkg/encoding/json/encode_test.go
に追加されています。
encode_test.go
には、ポインタレシーバを持つ MarshalJSON
を実装する Ref
型と、値レシーバを持つ MarshalJSON
を実装する Val
型が定義され、これらが構造体のフィールドとして含まれる場合に正しくマーシャリングされるかを確認する TestRefValMarshal
が追加されています。
// Ref has Marshaler and Unmarshaler methods with pointer receiver.
type Ref int
func (*Ref) MarshalJSON() ([]byte, error) {
return []byte(`"ref"`), nil
}
func (r *Ref) UnmarshalJSON([]byte) error {
*r = 12
return nil
}
// Val has Marshaler methods with value receiver.
type Val int
func (Val) MarshalJSON() ([]byte, error) {
return []byte(`"val"`), nil
}
func TestRefValMarshal(t *testing.T) {
var s = struct {
R0 Ref
R1 *Ref
V0 Val
V1 *Val
}{
R0: 12,
R1: new(Ref),
V0: 13,
V1: new(Val),
}
const want = `{"R0":"ref","R1":"ref","V0":"val","V1":"val"}`
b, err := Marshal(&s)
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if got := string(b); got != want {
t.Errorf("got %q, want %q", got, want)
}
}
decode_test.go
には、同様にポインタレシーバを持つ UnmarshalJSON
を実装する型が、アドレス可能な非ポインタ型として構造体フィールドに含まれる場合に正しくアンマーシャリングされるかを確認する TestRefUnmarshal
が追加されています。
コアとなるコードの解説
変更された encode.go
の reflectValueQuoted
関数内のロジックは以下のようになります。
-
m, ok := v.Interface().(Marshaler)
: まず、エンコード対象のreflect.Value
v
が、それ自体でMarshaler
インターフェースを満たしているかをチェックします。ok
がtrue
であれば、v
は直接Marshaler
を実装しているため、そのままm.MarshalJSON()
を呼び出す準備ができます。 -
if !ok { ... }
:v
が直接Marshaler
を満たさない場合、追加のチェックを行います。if v.Kind() != reflect.Ptr && v.CanAddr()
:v
がポインタ型ではなく、かつアドレス可能であるかをチェックします。これは、struct { F MyType }
のF
のようなケースに該当します。m, ok = v.Addr().Interface().(Marshaler)
: もしv
がポインタ型ではなくアドレス可能であれば、v.Addr()
を使ってv
のアドレス(ポインタ)を取得し、そのポインタがMarshaler
インターフェースを満たしているかをチェックします。if ok { v = v.Addr() }
: もしv
のアドレスがMarshaler
を満たすことが確認できれば、以降の処理でMarshalJSON
を呼び出すために、v
自体をそのアドレス(ポインタ)に置き換えます。これにより、ポインタレシーバを持つMarshalJSON
メソッドが正しく呼び出されるようになります。
-
if ok && (v.Kind() != reflect.Ptr || !v.IsNil()) { ... }
: 最終的にm
がMarshaler
インターフェースを満たしており、かつv
がポインタ型でないか、またはnil
ポインタでない場合に、m.MarshalJSON()
を呼び出します。この条件は、nil
ポインタに対してMarshalJSON
が呼び出されないようにするための既存のチェックです。
この変更により、encoding/json
パッケージは、ポインタレシーバを持つ MarshalJSON
メソッドが定義された型が、値として(ただしアドレス可能に)使用されている場合でも、そのカスタムマーシャリングロジックを正しく適用できるようになりました。
関連リンク
- Go Issue #2170: https://github.com/golang/go/issues/2170
- Go CL 5618045: https://golang.org/cl/5618045
参考にした情報源リンク
- Go issue 2170, titled "encoding/json: struct field implementing Unmarshaler,": https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHW61Hg5l83DnLrV1k_v1lrxydO3aNx0lwGDN-saGYvnWE1Vvg233NZjrzkTEo2NQJGfEk47BXx1yGIvHh6pJEFetApcjjT9XHUNfG37HEepyL-DLwo2t_oYzV83n0NFZh4bk8=
- stackoverflow.com (Go issue 2170 explanation): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF2gVi2ldIUbY1zm0FgOPi-qm_ZZL4q4OosIpOd4UexR2uiHHa1PsFa3jcjG14nxpamvOSgf92gltUcJmyvWL9KtqwgKvxVbpeqKRtz_zmG9scQlM0n9VL1Ih7j_weTMkhqKlEYxOh1Yg2swiBlRFCbHJGPccGx_mS_Wv7FfZ1YMCiQfTx3j0PPQ-zWo0MChFr3tPWTNylUi15h2wTeSTRqZ664lmNSjtz7DVsE6g==
- github.com (Go issue 71497 - encoding/json/v2 proposal): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGlVABtWlUDdlvbi0Mot3CV8F4himerr8xWauRL7O-mYsJrV1v6xl10H78uLx1LURiss4Gt0fGGIrvQDh1zJniugt_E4jRb3xCxT2c8YsnCMq-2zWZoSY735Dg2H6Ih5094ubvw
- Web search results for "Go encoding/json Marshaler interface pointer vs value receiver": (Provided by the tool, explains receiver types and interface satisfaction)