Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした