Goのメソッドのレシーバについて少し復習したので備忘代わりに少しまとめます。
Goにはclassはない
Goにはclassは存在せず、近い構造(型)の表現には struct
キーワードが利用できます。
type User struct {
Name string
}
型に紐づいた振る舞いはsturct定義の外にメソッド(レシーバを持つ関数)を書くことで実現できます。関数定義は func (レシーバ値 レシーバ型) 関数名
という形になります。 *T
で定義されたレシーバをポインタレシーバ、T
を値レシーバと呼びます。
type User struct {
Name string
}
// メソッド(ポインタレシーバ)
func (u *User)setName() {
u.Name = "gopher"
}
func main() {
u := new(User)
u.setName()
fmt.Println(u.Name) //=> gopherが出力される。
}
値レシーバとポインタレシーバの違い
端的にいうとメソッドの呼び出し時にレシーバのコピーが渡されるか、レシーバのポインタが渡されるかの違いです。試しにsetName
を値レシーバに変更すると、実行結果は空になります。これはメソッド呼び出し時に値がコピーされ呼び出し元には反映されない為です。実行してみると別のアドレスが表示されているのが分かると思います。 https://play.golang.org/p/ST7ySQdrCoI
type User struct {
Name string
}
// メソッド(値レシーバ)
func (u User) setName() {
fmt.Printf("u address is %p\n", &u)
u.Name = "gopher"
}
func main() {
u := new(User)
fmt.Printf("u address is %p\n", u)
u.setName()
fmt.Println(u.Name)
}
メソッド呼び出し時の暗黙的なレシーバ変換
ひとつ疑問が残ります。ここではstructの初期化にnew(T)
を利用しているので、uは*T
型になるはずです。何故値レシーバのメソッドが呼び出せるのでしょうか? これは、Goコンパイラが利便性の為に暗黙的な変換を行っているためです。ここではu.setName()
は(*u).setName()
と解釈されます。呼び出し元ではレシーバに変更が起きることを期待したくなりそうですが、レシーバは変更されないのでこの挙動には注意が必要です。
type User struct {
Name string
}
// メソッド(値レシーバ)
func (u User)setName() {
u.Name = "gopher"
}
func main() {
u := new(User) //uは*User型
u.setName() //暗黙的に(*u).setName()と解釈される。
fmt.Println(u.Name)
}
暗黙的な呼び出しの変換はポインタレシーバのメソッドに対しても適用されます。
type User struct {
Name string
}
// メソッド(ポインタレシーバ)
func (u *User)setName() {
u.Name = "gopher"
}
func main() {
u := User{} //uはUser型
u.setName() //暗黙的に(&u).setName()と解釈される。
fmt.Println(u.Name)
}
nilレシーバに対するメソッド呼び出し
またGoではnilレシーバのメソッドも呼び出しが可能ですが、呼び出し先でnilのレシーバを操作しようとするとpanic
が発生します。 (u.Name = "gopher"
の行で panic
が起きます)
package main
import (
"fmt"
)
type User struct {
Name string
}
// メソッド(ポインタレシーバ)
func (u *User) setName() {
u.Name = "gopher" // ここでpanic
}
func main() {
u := new(User)
u = nil
u.setName()
fmt.Println(u)
}
なので、呼び出し元でnilチェックを行うか、それを保証できない時は呼び出し先でnilチェックを行い適切にnilをハンドリングします。(といっていますが実際にnilチェックしてる例は少ない印象です。https://github.com/google/gvisor/blob/master/runsc/boot/network.go#L97)
package main
import (
"fmt"
)
type User struct {
Name string
}
// メソッド(ポインタレシーバ)
func (u *User) setName() {
//呼び出し先でのnilチェック
if u == nil {
return
}
u.Name = "gopher"
}
func main() {
u := new(User)
u = nil
u.setName()
fmt.Println(u)
}
メソッドのレシーバはポインタにすべきか値にすべきか
以下は、Goのコードレビュー時によく指摘されるコメント集「CodeReviewComments」の「Receiver Type」セクションを、DeepLで翻訳したものです。
メソッドで値レシーバを使用するかポインタレシーバを使用するかを選択するのは、特に新しいGoプログラマーにとっては難しいことです。
疑問がある場合はポインタを使用しますが、通常は効率性を考慮して、小さな不変構造体や基本型の値など、
値レシーバを使用することが理にかなっている場合もあります。いくつかの便利なガイドラインがあります。
- レシーバが map, func, chan の場合、それらへのポインタは使用しないでください。
- レシーバがスライスであり、メソッドがスライスの再スライスや再割り当てを行わない場合は、そのポインタを使用しないでください。
- メソッドがレシーバを変異させる必要がある場合、レシーバはポインタでなければなりません。
- レシーバが sync.Mutex などの同期フィールドを含む構造体である場合は、コピーを避けるためにレシーバはポインタでなければなりません。
- レシーバが大きな構造体や配列の場合は、ポインタ受信機の方が効率的です。大きいとはどのくらいの大きさでしょうか?その全要素をメソッドの引数として渡すのと同等だと仮定してください。それが大きすぎると感じる場合は、レシーバにとっても大きすぎるということになります。
- 関数やメソッドは、同時に、またはこのメソッドから呼び出されたときに、レシーバを変異させることができますか?値の型は、メソッドが呼び出されたときにレシーバのコピーを作成するので、外部からの更新がこのレシーバに適用されることはありません。元のレシーバで変更が見えるようにしなければならない場合は、レシーバはポインタでなければなりません。
- レシーバが構造体、配列、スライスのいずれかであり、その要素のいずれかが突然変異する可能性のあるものへのポインタである場合は、ポインタレシーバを使用した方が読み手にとって意図がより明確になります。
- レシーバが小さな配列や構造体で、当然のことながら値型(例えば time.Time 型のようなもの)であり、変異可能なフィールドやポインタを持たない場合、あるいは int や string のような単純で基本的な型である場合は、値レシーバを使用するのが理にかなっています。値レシーバは、生成されるガベージの量を減らすことができます。値が値メソッドに渡された場合、ヒープ上に確保する代わりにオンスタックコピーを使用することができます。(コンパイラはこの割り当てを避けるために賢くなろうとしますが、常に成功するとは限りません)。この理由から、最初にプロファイリングを行わずに値のレシーバ型を選択しないでください。
- 最後に、疑わしい場合はポインタレシーバを使用してください。
最後に メソッドのレシーバはポインタにすべきか値にすべきか
という論点について、いくつか良い参考資料を見つけたので貼っておきます。Goはパフォーマンスを考慮しているからか、ポインタレシーバを基本とする考えが強いように思います。CodeReviewComments - Receiver Type と golang の 引数、戻り値、レシーバをポインタにすべきか、値にすべきかの判断基準について迷っている が特に参考になると思います。