この記事は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
だけではなく、型T
のf
も含みます。
そのため、(&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道場では、このように実際の業務でハマりやすいポイントを解説を踏まえながら説明するようにしています。
興味を持たれた方は次回の開催ではぜひ参加してみてください。