Menu

Go言語のメソッド:値レシーバーとポインタレシーバーの違い

メソッドはレシーバーを持つ関数です。メソッドの宣言方法、値レシーバーとポインタレシーバーの使い分け、メソッドセットとインターフェース、構造体以外の型へのメソッドを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

メソッドを宣言する

メソッドはレシーバーを持つ関数です。レシーバーはメソッド名の前に書く追加の引数で、メソッドを型に結びつけます。

レシーバーの名前は、たいてい型から取った1、2文字にします(Circle なら c)。Goは thisself を使わず、スタイルガイドもそうした名前を勧めていません。

CircleString() string メソッドを持っているので fmt.Stringer を満たし、fmt.Println{2} ではなくその結果を表示します。

値レシーバーとポインタレシーバー

値レシーバー (c Circle) はコピーを受け取ります。ポインタレシーバー (c *Circle) はアドレスを受け取るので、フィールドへの変更が残ります。

IncByValue はコンパイルできますが、目に見える効果は何もありません。典型的なバグです。レシーバーを変更するためのメソッドには、ポインタレシーバーが必要です。

この呼び出しは、Goが提供する2つの便利な仕組みも示しています。アドレスを取れる変数に対する c.Inc()(&c).Inc() に書き換えられ、ポインタに対する p.Value()(*p).Value() に書き換えられます。メソッドを呼ぶためだけに &* を書くことはめったにありません。

自動的な & には、アドレスを取れるものが必要です。マップの要素や複合リテラルはそうではありません。

m := map[string]Counter{"a": {}}
m["a"].Inc()    // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable

マップにポインタを格納する(map[string]*Counter)か、値を取り出して変更し、書き戻します。

どちらのレシーバーを選ぶか

ポインタレシーバーを使うのは値レシーバーを使うのは
メソッドがレシーバーを変更するとき型が小さく、メソッドで変更されないとき(time.Time、座標など)
構造体が大きいとき(コピーにコストがかかる)型がマップ、関数、チャネルのとき(すでに参照)
型が sync.Mutex などを持つとき値をプリミティブのように扱い、安全にコピーしたいとき
型の他のメソッドがすでにポインタを使っているとき

GoのFAQと標準ライブラリは1つの経験則に従っています。一貫させることです。どれか1つのメソッドにポインタレシーバーが必要なら、すべてのメソッドをポインタレシーバーにして、メソッドセットを予測しやすくします。go vet は、sync.Mutex を持つ構造体が値レシーバーでコピーされていると指摘します(passes lock by value)。

メソッドセットとインターフェース

この違いが最も重要になるのはインターフェースです。T のメソッドセットには値レシーバーのメソッドが含まれます。*T のメソッドセットには、値レシーバーとポインタレシーバーの両方のメソッドが含まれます。

このスライスで & を付けずに Square{Side: 3} と書くと、コンパイルに失敗します。

cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)

理由は、インターフェースはアドレスを取れない Square のコピーを保持しうるので、Goがポインタメソッドを呼ぶためにそのアドレスを取れないからです。続きはインターフェースのページを参照してください。

構造体以外の型のメソッド

構造体に限らず、自分のパッケージで宣言した名前付きの型ならどれでもメソッドを持てます。

制限は次のとおりです。型はメソッドと同じパッケージで定義されている必要があり、基底型がポインタやインターフェースであってはいけません。自分のパッケージから intstringtime.Time にメソッドを追加することはできません。新しい名前付きの型で包むのがGoのやり方です。整数型の String() は、列挙型を読みやすく表示する方法でもあります。iotaを使った列挙型のページを参照してください。

メソッド値とメソッド式

特定のレシーバーに束縛されたメソッドは、関数の値になります。

値レシーバーのメソッド値は作られたときにレシーバーをコピーするので、f はAnaのままです。ポインタレシーバーならポインタを保存し、後の変更が見えます。メソッド値はコールバックに便利です:http.HandleFunc("/", srv.handleIndex)

よくある間違い

  • 変更するメソッドに値レシーバーを使う。 コンパイルは通り、何も言わずにコピーを変更します。
  • レシーバーの種類を混ぜる。 T*T のどちらがインターフェースを満たすのかがわかりにくくなります。型ごとにどちらか一方を選びます。
  • nilポインタでポインタメソッドを呼ぶ。 これは許されていて、メソッドはnilのレシーバーで実行されます。panicするのはフィールドに触れたときだけです。一部の型(nilに安全な連結リストなど)はこれを意図的に使いますが、ほとんどの型は想定していません。

よくある質問

Goのメソッドと関数の違いは何ですか?

メソッドは func と名前の間に書くレシーバーを持ちます:func (c Circle) Area() float64。値に対して呼び(c.Area())、型のメソッドセットに属し、型がインターフェースを満たす手段になります。それ以外は、最初の引数がレシーバーである関数と同じように振る舞います。

Goでポインタレシーバーを使うべきなのはいつですか?

メソッドがレシーバーを変更する必要があるとき、呼び出しのたびにコピーするのが無駄なほど構造体が大きいとき、sync.Mutex のようにコピーしてはいけないものを型が含むときに使います。型のメソッドのどれか1つでもポインタレシーバーが必要なら、一貫性のためにすべてをポインタレシーバーにするのが一般的な助言です。

メソッドがポインタレシーバーだと、なぜ型がインターフェースを実装しないのですか?

ポインタレシーバーのメソッドは T ではなく *T のメソッドセットに属するからです。そのため var s Shape = Square{}Square does not implement Shape (method Area has pointer receiver) で失敗します。代わりにポインタを代入します:var s Shape = &Square{}

Goで組み込み型にメソッドを定義できますか?

直接はできません。メソッドは同じパッケージで定義された名前付きの型にしか宣言できません。まず type Celsius float64 のように独自の型を定義し、それにメソッドを追加します。intstring、他のパッケージの型にはメソッドを追加できません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める