「レシーバ」とは
Go 言語はある種のオブジェクト指向プログラミング (OOP) 言語であり、 OOP 言語の慣例通り、メソッドを呼び出される対象のことを「レシーバ」と呼びます。
1 | p := Person{Name: "Taro"} |
ちなみになぜ「レシーバ」と呼ぶのかというと、昔の OOP 言語の文脈ではメソッド呼び出しのことを「メッセージの送信」と言い、メソッドを呼び出される側は「メッセージの受信側」だからです。
「値レシーバ」と「ポインタレシーバ」
Go 言語では「値」と「ポインタ」が明示的に区別されているため、たとえばある構造体に対してメソッドを定義する場合でも、「値型」に対する定義なのか「ポインタ型」に対する定義なのかはっきりと区別しなければなりません。それぞれについて簡単に説明します。
値レシーバ
「値型」に対してメソッド定義されたものが「値レシーバ」です。 Go 言語では構造体は値なので、以下の例では Person
という値型に対して Greet()
というメソッドを定義しています。
1 | type Person struct{ Name string } |
ポインタレシーバ
「ポインタ型」に対してメソッド定義されたものが「ポインタレシーバ」です。以下の例では *Person
というポインタ型に対して Shout()
というメソッドを定義しています。
1 | type Person struct{ Name string } |
コンパイラによるレシーバの暗黙的変換
しかし実際には、メソッド呼び出し時にこのあたりのことを意識しなくてもすみます。それは Go 言語仕様の「呼び出し」のセクションにある通り、レシーバの【値型 ⇔ ポインタ型】間の変換はコンパイラが暗黙的におこなってくれるからです。それぞれ例を見てみましょう。
値レシーバの場合
値型に対してあるメソッドが定義されているときに、ポインタ型変数からそのメソッドを呼び出そうとすると、コンパイラが暗黙的に値型のメソッド呼び出しに変換してくれます。以下の例では、 値型 Person
に Greet()
が定義されていますが、 *Person
型変数 pp
からでも問題なく呼び出せます。
1 | type Person struct{ Name string } |
したがって、以下のように nil
ポインタ変数から呼び出そうとすると panic
を起こします。 *nilp
が存在しないからです。
1 | var nilp *Person // nil ポインタ変数だと... |
これは Greet()
の中で p.Name
を使っていることとは関係なく発生します。
上記コードの Playground: http://play.golang.org/p/SZOv0hTicF
ポインタレシーバの場合
一方、ポインタ型に対してあるメソッドが定義されているときに、値型変数からそのメソッドを呼び出そうとすると、コンパイラが暗黙的にポインタ型のメソッド呼び出しに変換してくれます。以下の例では、 ポインタ型 *Person
に Shout()
が定義されていますが、 Person
型変数 p
からでも問題なく呼び出せます。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/Vs-LOJq_1d
さらに、ポインタレシーバのメソッドは nil
ポインタ変数からでも呼び出しが可能です。
1 | var nilp *Person // nil ポインタ変数でも... |
ただし当然のことながら、メソッド内でフィールドを使っていたら参照先がないので panic
になります。以下の例では、 p.Name
を使っているメソッド ShoutName
を nil
ポインタ変数から呼び出すと、メソッド呼び出しそのものは正常におこなわれるものの、フィールド呼び出しがあるせいで panic
になります。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/UzB32jpi2q
メソッド定義とメソッド呼び出しの真実
メソッド定義の真実
メソッド定義は本質的には関数定義と等価です。 Go 言語の場合
1 | func (p Person) Greet(msg string) { |
というメソッド定義は、内部的には
1 | func Person.Greet(p Person, msg string) { |
という「メソッド式 (method expression)」と呼ばれる関数として定義されます。これは値レシーバの場合で、同様にポインタレシーバの場合
1 | func (pp *Person) Shout(msg string) { |
は
1 | func (*Person).Shout(pp *Person, msg string) { |
という関数として定義されます。
コンパイラのソースコードを確かめる
Go 1.5.2 のソースコードを基にこのことを確かめてみましょう。該当箇所は以下になります (コメントはすべて僕が追記したものです)。
src/cmd/compile/internal/gc/dcl.go#L1325-L1350
1 | // n: メソッドを表す AST ノード |
上記コードの変数 p
を見ていただくとわかる通り、メソッド定義においてメソッド名は (*Type).Method
または Type.Method
という名前をもつ、関数の AST ノードに変換されています。
メソッド呼び出しの真実
メソッド定義と同様に、メソッド呼び出しもメソッド式という関数の呼び出しの糖衣構文にすぎません。たとえば、以下の例では p.Greet(...)
と Person.Greet(p, ...)
、 pp.Shout(...)
と (*Person).Shout(pp, ...)
はそれぞれ等価です。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/3ENNQWghCX
ちなみに、メソッド式やメソッドは値としても扱えるので、以下のように変数に代入して利用することもできます。この場合、メソッド式 f
にはレシーバ引数が必要で、メソッド値 g
には必要ないことに注意してください。
1 | type Person struct{ Name string } |
上記コードの Playground: http://play.golang.org/p/kpaGwzPu1u
Go は関数が第一級オブジェクトなので便利ですね。
メソッド定義で意識すべきこと
改めて強調すると、メソッド定義において意識すべきなのは、レシーバが「第0引数」であるということです。たとえば、以下のメソッド定義
1 | type Person struct{ Name string } |
は、内部的には
1 | func Person.Greet(p Person, msg string) { |
というメソッド式(関数)として定義されます。ただし直接このような関数名での関数定義はできません(コンパイラが .
を含むシンボル名を許していないので)。
そして、メソッド呼び出しがメソッド式呼び出しと等価ということは、メソッド定義をする際には以下のことに気をつける必要があります。
1. メソッド呼び出しごとにレシーバの値はコピーされる
関数呼び出しごとに引数の値はスタックにコピーされますので、当然メソッド呼び出し時にはレシーバの値もコピーされます。
1 | p := Person{Name: "Taro"} |
したがって、特にデータ量の大きな構造体に値レシーバのメソッドを定義すると、メソッド呼び出しごとにコピーが発生するので非常に非効率であることがわかります。このことから、構造体におけるメソッド定義は原則ポインタレシーバに対しておこなったほうがよいです。
2. 値レシーバの値はメソッド内で書き換えても元のレシーバの値にはまったく影響がない
値レシーバの場合、値そのものがまるっとコピーされるので、メソッド内でいくら値を書き換えても元のレシーバの値にはまったく影響がありません。
以下の例の場合、 UnchangeName()
の処理は一見 setter っぽくレシーバの name
を書き換えているように見えますが、 p
が値レシーバなので p.Greet()
で表示される名前は Jiro
ではなく Taro
のままになります。
1 | type Person struct{ name string } |
上記コードの Playground: http://play.golang.org/p/5kJVlx_XY7
このことを利用すると、不変オブジェクトをつくることができます。その典型例が time
パッケージの Time
です。
time.Time
は不変オブジェクト
time.Time
の API 一覧を見てもらうとわかる通り、 Time
オブジェクトは
- フィールドがすべてプライベート
- レシーバは Unmarshal 系を除きすべて
Time
型(値型) - オブジェクトを返すメソッドの戻り値もすべて
Time
型(値型)
となっており、 *Time
型(ポインタ型)は基本的に現れません。
フィールドがプライベートなので直接の値の変更ができず、またレシーバも戻り値もほぼすべてポインタ型ではなく値型なので、一度生成された Time
オブジェクトは一切変更する手段がないということになります。
Java では java.util.Date
クラスや java.util.Calendar
クラスが可変でスレッドセーフではないことが長年問題となっており、やっと Java 8 で不変オブジェクトを基本とする java.time
パッケージが導入されました(Java SE 8 Date and Time -新しい日付/時間ライブラリが必要になる理由-)。 Go では少なくともこの種の問題は避けられているわけですね。
それでも Go ではエクスポートされたフィールドの直接書き換えや、オブジェクトの内部状態を書き換える副作用のあるメソッドが気軽に用いられる傾向にあります。並行処理に耐える不変オブジェクトを設計したい場合は、 Time
を参考にした構造体設計にするとよいかもしれません。
3. ポインタレシーバにするとメソッド内でレシーバの値を書き換えられる
一方、レシーバをポインタレシーバにした場合、メソッド呼び出し時にコピーされるのもポインタになるので、そのポインタを使うことでレシーバの実際の値を書き換えることができます(ただし map
や chan
などの参照型をベースとする型は半分ポインタみたいなものなので、値レシーバでも可能ですが)。
たとえば以下の例の場合、 ChangeName()
メソッドのレシーバは *Person
なので、 p.name
フィールドを書き換えることが可能であり、 p.Greet()
で表示される名前は Jiro
に変化します。
1 | type Person struct{ name string } |
上記コードの Playground: http://play.golang.org/p/asH9DZRmxG
逆に言うと、レシーバの内部状態を変更したいメソッドは、(参照型を除き)必ずポインタレシーバで定義しなければなりません。
4. ポインタレシーバのメソッド内でフィールドを呼び出す場合には、その前に nil
チェックをすべき
ポインタレシーバのメソッドは nil
ポインタ変数からでも呼び出しが可能なため、ポインタレシーバのメソッド内でフィールドを呼び出している場合、常に nil pointer dereference
で panic
になる危険性を孕んでいます。
以下の例の場合、 ChangeNameUnsafe()
には nil
チェックがないため panic
になりますが、 ChangeNameSafe()
のほうは nil
チェックをしているため安全です。
1 | type Person struct{ name string } |
レシーバは他の言語でいう this
や self
などに相当するため、メソッド内で nil
チェックをすることには違和感があるかもしれません。しかし「レシーバは第0引数」であることを思い出せば、他のポインタ引数の nil
チェックと同様に考えて、安全なプログラムを書くためにきちんとやったほうがよいです(僕もよく忘れますが)。
逆に考えると、 Java などと違いメソッド内でレシーバが nil
か否かに応じたハンドリングができるので、それがきちんとなされていれば、メソッド呼び出し側が事前の nil
チェックをする必要がないありがたさもあります。
レシーバの値を使わない場合はレシーバ変数を記述しなくてもよい
上記のことに関連して、実はメソッド定義におけるレシーバ変数は書かなくても問題なくコンパイルできるため、メソッド内でレシーバの値を使わない場合は書かないのも1つの手です。
1 | type Person struct{ name string } |
そのような処理は本質的にはメソッドにする必要がなく、単なる関数のままでもよいため、このようなケースはあまり多くないとは思います。ただ、パッとシグニチャを見ただけでレシーバの値が使われていないことがわかるので、ドキュメントとしての価値もあると思います。
まとめ:値レシーバとポインタレシーバの使い分け
最後に、値レシーバとポインタレシーバの使い分けについて整理しておきたいと思います。
基本はポインタレシーバ
レシーバの値を変更したい場合は(参照型を除き)必ずポインタレシーバにしなければなりません。構造体も基本的にはポインタレシーバにしたほうがよいでしょう。
また、統一性の観点からも、ある型のメソッドのレシーバを1つでもポインタレシーバにした場合には、値の変更の有無にかかわらずすべてポインタレシーバにしたほうがよいと思います。
値レシーバにしたほうがよい場合
レシーバの値を変更する必要がなく、かつ以下のどれかに該当するときは値レシーバにしたほうがよいです。ただし参照型の場合は値の変更の有無は関係ありません。
int
, string
などのプリミティブ型をベースとする型
プリミティブ型はコピーコストが小さいため、通常は値レシーバにします。
map
, chan
といった参照型をベースとする型
参照型は半分ポインタみたいなものなので、値レシーバのままでも保持する要素の値を変更できます。コピーコストも大きくないため、通常は値レシーバにします。
不変型
先に例として出した time.Time
のような不変型を定義したい場合には、すべて値レシーバとして定義するのも良い方法です。
小さい構造体
コピーコストの小さい構造体の場合には、値レシーバでメソッドを定義すると処理がスタックで完結するので、ヒープへのアロケート回数や GC の回数が減ることが期待できます。
ただしこの使い分けは感覚的におこなうのではなく、本当に効率的になるのかきちんと計測した結果に基いておこなうべきです。 Go 言語ではベンチマークの計測や pprof などによる解析が容易におこなえるので、これらを活用するとよいでしょう。