LoginSignup
77
59

More than 3 years have passed since last update.

Goのstructやレシーバについての備忘録

Last updated at Posted at 2018-04-25

Goのメソッドのレシーバについて少し復習したので備忘代わりに少しまとめます。

Goにはclassはない

Goにはclassは存在せず、似たような構造を表現するのにはstructキーワードが利用できます。

type User struct {
    Name string
}

また、メソッドの定義はstruct定義の外に関数を書くことになります。
関数定義は 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が発生します。
なので、呼び出し元で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)
}

メソッドのレシーバはポインタにすべきか値にすべきか

以下はCodeReviewComments - Receiver TypeをDeepLに突っ込んだものです。

メソッドで値レシーバを使用するかポインタレシーバを使用するかを選択するのは、特に新しいGoプログラマーにとっては難しいことです。
疑問がある場合はポインタを使用しますが、通常は効率性を考慮して、小さな不変構造体や基本型の値など、
値レシーバを使用することが理にかなっている場合もあります。いくつかの便利なガイドラインがあります。

- レシーバが map, func, chan の場合、それらへのポインタは使用しないでください。
- レシーバがスライスであり、メソッドがスライスの再スライスや再割り当てを行わない場合は、そのポインタを使用しないでください。
- メソッドがレシーバを変異させる必要がある場合、レシーバはポインタでなければなりません。
- レシーバが sync.Mutex などの同期フィールドを含む構造体である場合は、コピーを避けるためにレシーバはポインタでなければなりません。
- レシーバが大きな構造体や配列の場合は、ポインタ受信機の方が効率的です。大きいとはどのくらいの大きさでしょうか?その全要素をメソッドの引数として渡すのと同等だと仮定してください。それが大きすぎると感じる場合は、レシーバにとっても大きすぎるということになります。
- 関数やメソッドは、同時に、またはこのメソッドから呼び出されたときに、レシーバを変異させることができますか?値の型は、メソッドが呼び出されたときにレシーバのコピーを作成するので、外部からの更新がこのレシーバに適用されることはありません。元のレシーバで変更が見えるようにしなければならない場合は、レシーバはポインタでなければなりません。
- レシーバが構造体、配列、スライスのいずれかであり、その要素のいずれかが突然変異する可能性のあるものへのポインタである場合は、ポインタレシーバを使用した方が読み手にとって意図がより明確になります。
- レシーバが小さな配列や構造体で、当然のことながら値型(例えば time.Time 型のようなもの)であり、変異可能なフィールドやポインタを持たない場合、あるいは int や string のような単純で基本的な型である場合は、値レシーバを使用するのが理にかなっています。値レシーバは、生成されるガベージの量を減らすことができます。値が値メソッドに渡された場合、ヒープ上に確保する代わりにオンスタックコピーを使用することができます。(コンパイラはこの割り当てを避けるために賢くなろうとしますが、常に成功するとは限りません)。この理由から、最初にプロファイリングを行わずに値のレシーバ型を選択しないでください。
- 最後に、疑わしい場合はポインタレシーバを使用してください。

最後に メソッドのレシーバはポインタにすべきか値にすべきか ですが、いくつか良い参考資料を見つけたのではっておきます。Goはパフォーマンスを考慮しているからか、ポインタレシーバを基本とする考えが強いように思います。CodeReviewComments - Receiver Typegolang の 引数、戻り値、レシーバをポインタにすべきか、値にすべきかの判断基準について迷っているが特に参考になると思います。

参考

77
59
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
77
59