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

[インデックス 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種類があります。

  1. 値レシーバ (func (t MyType) MethodName()):

    • メソッドが呼び出される際、レシーバの値のコピーがメソッドに渡されます。
    • メソッド内でレシーバの値を変更しても、元の値には影響しません。
    • MyType 型の変数も *MyType 型の変数も、値レシーバを持つメソッドを呼び出すことができます。Goは必要に応じて自動的に値のコピーを作成したり、ポインタをデリファレンスしたりします。
    • インターフェースの観点では、型 MyType が値レシーバでインターフェースを実装している場合、MyType*MyType の両方がそのインターフェースを満たします。
  2. ポインタレシーバ (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 を呼び出す ...
}

ここで vreflect.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.gosrc/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.goreflectValueQuoted 関数内のロジックは以下のようになります。

  1. m, ok := v.Interface().(Marshaler): まず、エンコード対象の reflect.Value v が、それ自体で Marshaler インターフェースを満たしているかをチェックします。oktrue であれば、v は直接 Marshaler を実装しているため、そのまま m.MarshalJSON() を呼び出す準備ができます。

  2. 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 メソッドが正しく呼び出されるようになります。
  3. if ok && (v.Kind() != reflect.Ptr || !v.IsNil()) { ... }: 最終的に mMarshaler インターフェースを満たしており、かつ v がポインタ型でないか、または nil ポインタでない場合に、m.MarshalJSON() を呼び出します。この条件は、nil ポインタに対して MarshalJSON が呼び出されないようにするための既存のチェックです。

この変更により、encoding/json パッケージは、ポインタレシーバを持つ MarshalJSON メソッドが定義された型が、値として(ただしアドレス可能に)使用されている場合でも、そのカスタムマーシャリングロジックを正しく適用できるようになりました。

関連リンク

参考にした情報源リンク