Edited at

メソッドとレシーバについてのまとめ #golang

この記事はGopher道場アドベントカレンダーの1日目の記事です。


ポインタをレシーバにする場合

Goでは次のようにtypeで型を定義し、

レシーバを指定することでメソッドを定義することができます。

type T int

func (t *T) M() {
fmt.Println("method")
}

メソッドを呼び出すためには、次のようにセレクタでアクセスし呼び出します。

type T int

func (t *T) M() {
fmt.Println("method")
}

func main() {
var t T
p := &t
p.M()
}

レシーバの型がポインタであれば、次のように呼び出しても構いません。

次の例では、t.M()(&t).M()と同じように解釈されます。

type T int

func (t *T) M() {
fmt.Println("method")
}

func main() {
var t T
t.M() // (&t).M()と同じ
}

この例ではT型にメソッドMが紐付いているわけではありません。

そのため、次のコードを実行すると102030ではなく303030が表示されます。

package main

type T int
func (t *T) M() {
fmt.Print(int(*t))
}

func main() {
ts := []T{10, 20, 30}

ms := make([]func(), 0, len(ts))
for _, t := range ts {
ms = append(ms, t.M)
}

// 303030と表示される
for _, m := range ms {
m()
}
}

このコードを分かりやすく書き直してみると次のようになります。

forの中でtのポインタは常に同じなので、(&t).Mもずっと同じメソッド値を

スライスにappendしていることになります。

package main

type T int
func (t *T) M() {
fmt.Print(int(*t))
}

func main() {
ts := []T{10, 20, 30}

ms := make([]func(), 0, len(ts))
for _, t := range ts {
// tはforの中でずっと同じポインタ
ms = append(ms, (&t).M)
}

// 303030と表示される
for _, m := range ms {
m()
}
}

元ネタ:https://twitter.com/inukirom/status/1065520332411363328


メソッドセット

Tが持つメソッドセットと型*Tの持つメソッドセットは次のようになっています。


  • *Tのメソッドセットには、型Tのメソッドも含む

  • Tのメソッドセットには、型*Tのメソッドセットは含まない

例えば、次の例の場合型Tのメソッドセットはfのみですが、

*Tのメソッドセットはgだけではなく、型Tfも含みます。

そのため、(&T{}).f()などは実行できますが、(T{}).g()は実行できません。

func (t T) f()  {}

func (t *T) g() {}
func main() {
(T{}).f()
(&T{}).f()
(*&T{}).f()

(T{}).g() // <- できない
(&T{}).g()
(*&T{}).g()
}

一方で、次のようにT{}を一度変数に入れることにより、

&tのようにポインタを取れるようになるため、t.g()(&t).g()と解釈されます。

しかし、あくまで型Tのメソッドセットには、gは含まれないということには注意しましょう。

func (t T) f()  {}

func (t *T) g() {}
func main() {
// できない
(T{}).g()

// できる
t := T{}
t.g() // (&t).g()と解釈
}

Goではあるインタフェースのメソッドセットがある型のメソッドセットの部分集合のとき、

その型はそのインタフェースを実装していることになります。

そのため、次の例では、型Tはメソッドセットにgを含まないため、

インタフェースIを実装していることにはなりません。

type I interface { g() }

func (t T) f() {}
func (t *T) g() {}
func main() {
t := T{}
// (&t).g()と解釈されるので呼べる
t.g()

// 型Tはgを持たないので代入できない
var i I = t
}


まとめ

このように、レシーバとポインタにまつわる問題は発見しづらいため、

レシーバをポインタにした場合は変数(ゼロ値で扱いたい時を除き)やスライスの要素はポインタにすべきでしょう。

Gopher道場では、このように実際の業務でハマりやすいポイントを解説を踏まえながら説明するようにしています。

興味を持たれた方は次回の開催ではぜひ参加してみてください。