🕌

Goのencoding/jsonに学ぶinterface型の罠

2024/04/07に公開

非公開フィールドを含む構造体にMarshalJSON()メソッドを実装したのにjsonへ変換されなかったことがあったので、なぜそうなったのかを調べました。

調べていく過程でinterfaceと実体の変換についての言語仕様を確認する機会になるので、初学者の方や再確認したい方にとって有益な記事になっていると嬉しいです。

前提

簡単に前提知識を記載しておきます。

json.Marshal()で構造体からjsonへのマッピングを行えます。しかし、構造体のフィールドは公開されていないと取得できないため、非公開のフィールドをjsonにmarshalしたい時は、MarshalJSON()メソッドを実装する必要があります。
その構造体をjson.Marshal()に渡してあげると独自で実装したMarshalJSON()に基づいてjsonへの変換してくれます。
(Stringerインターフェースを実装すると文字列として出力する際にString()の結果が出るみたいな感じです)

問題のコード

非公開フィールドをjsonにマッピングするために、非公開フィールドnameをもつDog構造体にMarshalJSON()メソッドを実装しました。
公開フィールドをもつアノニマス構造体を定義してそれをjson.Marshal()に渡すというものです。

type Dog struct {
	name string
}

func (d *Dog) MarshalJSON() ([]byte, error) {
	d2 := struct {
		Name string `json:"name"`
	}{
		Name: d.name,
	}
	return json.Marshal(d2)
}

これを値、ポインタの両方でmarshalしてみます。
値の方は空のjsonになってしまいました。

func main() {
	dogValue := Dog{name: "kotetsu"}
	dogPointer := &Dog{name: "kotetsu"}

	resValue, _ := json.Marshal(dogValue)
	resPointer, _ := json.Marshal(dogPointer)

	fmt.Printf("dog value: %s\n", string(resValue))     // dog value: {}
	fmt.Printf("dog pointer: %s\n", string(resPointer)) // dog pointer: {"name":"kotetsu"}
}

goplayground

Goではポインタレシーバのメソッドを値の変数から呼び出しても変換してくれます。(逆もそうです)
なのでメソッド呼び出しの挙動に差異はないはずです。なんでこうなったのでしょうか?

type A struct {}

func (a A) ValueReceiverMethod() {
  fmt.Println("called value receiver method")
}
func (a *A) PointerReceiverMethod() {
  fmt.Println("called pointer receiver method")
}

func main() {
  v := A{}
  p := &A{}

  v.ValueReceiverMethod()   // OK
  v.PointerReceiverMethod() // OK (&v).PointerReceiverMethod()として解釈される
  p.ValueReceiverMethod()   // OK (*p).ValueReceiverMethod()として解釈される
  p.PointerReceiverMethod() // OK
}

goplayground

interface型への変換

空のjsonになってしまう原因はinterface型との変換時の言語仕様にありました。

下のコードの var i2v I2 = vはコンパイルエラーになります。
ポインタレシーバでインターフェースを実装すると、実体が値の場合は実装していないことになります(①)。メソッド呼び出しの際の自動変換のようにvar i2v I2 = &vとして解釈してはくれません。
しかし、値レシーバでの実装の場合は違っていて、実体が値でもポインタでも実装していることになります(②)。

また、型アサーション時にも同様のことが言えて、実体が値の場合はポインタレシーバは実装していないことになる(③)のですが、実体がポインタの場合は値レシーバでも実装していることになります(④)。

type A struct{}

func (a A) M1() {
	fmt.Println("called value receiver method")
}
func (a *A) M2() {
	fmt.Println("called pointer receiver method")
}

type I1 interface {
	M1()
}
type I2 interface {
	M2()
}

func main() {
	// interface型への代入
	v := A{}
	p := &A{}
	var i1v I1 = v
	var i2v I2 = v // ① A does not implement I2 (method M2 has pointer receiver)
	var i1p I1 = p // ② ok M1は値レシーバだが、pを代入できる
	var i2p I2 = p
	i1v.M1()
	// i2v.M2()
	i1p.M1()
	i2p.M2()

	// interface型の型アサーション
	var iv interface{} = v
	var ip interface{} = p
	if _, ok := iv.(I1); !ok {
		fmt.Println("iv does not implement I1")
	}
	if _, ok := iv.(I2); !ok {
		fmt.Println("iv does not implement I2") // ③  到達する
	}
	if _, ok := ip.(I1); !ok {
		fmt.Println("ip does not implement I1") // ④
	}
	if _, ok := ip.(I2); !ok {
		fmt.Println("ip does not implement I2")
	}

}

goplayground

表でまとめると以下のようです。

インターフェースの実体 値レシーバ ポインタレシーバ
o (当然) x(まあわかる)
ポインタ o(!??) o(当然)

json.Marshal()で型アサーションをしている

json.Marshal()の内部コードを探っていくと、Marshalerインターフェースがあります。

type Marshaler interface {
	MarshalJSON() ([]byte, error)
}

また、interfaceをMarshalerに型アサーションをして、MarshalJSON()を読んでいます。

func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) {
  // 略
	m, ok := v.Interface().(Marshaler)
	if !ok {
		e.WriteString("null")
		return
	}
	b, err := m.MarshalJSON()
	// 略
}

これまでを踏まえて問題のコードに戻る

json.Marshal()に渡したdogValueは値なので、ポインタレシーバのMarshalJSON()は実装されていない、つまりMarshalerインターフェースを実装していないことになっていたのです。
そのため、非公開フィールドのnameが見えず空のjsonになっていたのでした。

type Dog struct {
	name string
}

func (d *Dog) MarshalJSON() ([]byte, error) {
	d2 := struct {
		Name string `json:"name"`
	}{
		Name: d.name,
	}
	return json.Marshal(d2)
}

func main() {
	dogValue := Dog{name: "kotetsu"}

	resValue, _ := json.Marshal(dogValue) // dogValueはMarshalerインターフェースを実装できていない

	fmt.Printf("dog value: %s\n", string(resValue))     // dog value: {}
}

まとめ

インターフェースの実体が値の時にポインタレシーバは実装されていないことになります。
インターフェースとして抽象化したい場合は実体はポインタにしておくのが想定外の挙動にならなくて安全かもしれません。

ぼやき
自動変換のおかげで余計分かりづらくなっている気がしてしまいます。

参考

https://go-tour-jp.appspot.com/methods/6
https://go-tour-jp.appspot.com/methods/7
https://tech.yappli.io/entry/methods-and-pointer-indirection-in-go
https://soranoba.net/programming/interface-and-receiver

GitHubで編集を提案

Discussion