2017/09/27更新

[Go] 構造体で、値メソッドとポインタメソッドを使い分ける

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
前回(Goのポインタを学ぶ)に引き続き、今日はGo言語の構造体とポインタについて、ブログに書きたいと思います。
画像


目次




構造体に振る舞いを追加する

下記のサンプルコードは、https://play.golang.org/p/B7pWW8dm8Oで試すことができます。


構造体にメソッドを追加することで、振る舞いを定義することができます。例えば以下のようなユーザーを表現する構造体があるとします。
type User struct {
    Name string
}
以下のように、メソッドを追加することで「挨拶をする」振る舞いを定義することができます。
func (u User) Greeting(msg string) {
    fmt.Printf("%s by %s\n", msg, u.Name)
}

// 実行してみると
user := User{ "Yohei" }
user.Greeting("Hello")   // Hello by Yohei
そして、同じような振る舞いを、ポインタメソッドとして定義することもできます。上記との違いは関数定義の際に(u *User)と、構造体のポインタに対して振る舞いを追加している点です。
func (u *User) Greeting2(msg string) {
    fmt.Printf("%s to %s\n", msg, u.Name)
}

// 実行してみる
user := User{ "Yohei" }
user.Greeting2("Hello") // Hello to Yohei
このように、構造体への振る舞いの追加は、2種類の方法で行うことができます。



ポインタメソッドを使う必要がある場合

さて、上記のGreetingメソッドであれば、値関数でもポインタ関数でもどちらでも良いのですが、ポインタメソッドを使う必要がある場面が存在します。

それは、メソッド内で構造体の値を書き換える必要がある場合、です。

前回のブログでも見たように、値関数で呼び出した場合には、関数呼び出し時に値がコピーされて、呼び出し元/先で別のメモリを参照します。逆に、ポインタ関数の場合には、関数呼び出し前後で同じメモリを共有します。
// https://play.golang.org/p/gCLOVLtHeU

// 値関数で振る舞いを定義
func (u User) Greeting(msg string) {
    fmt.Println("User.Name(関数内):", &(u.Name))
}

// ポインタ関数で振る舞いを定義
func (u *User) Greeting2(msg string) {
    fmt.Println("User.Name(関数内):", &(u.Name))
}

func main() {
    
    user := User{ "Yohei" }
    
    // 値関数の呼び出し
    fmt.Println("User.Name:", &(user.Name))  // User.Name: 0x1040c128
    user.Greeting("Hello")                   // User.Name(関数内): 0x1040c138(違う!!)

    // ポインタ関数の呼び出し
    fmt.Println("User.Name:", &(user.Name))  // User.Name: 0x1040c128
    user.Greeting2("Hello")                  // User.Name(関数内): 0x1040c128(同じ!!)
}
そのため、関数内で構造体の値を変更した場合に、値関数とポインタ関数で振る舞いが異なります。
https://play.golang.org/p/acEcWway55
type User struct {
    Name string
}

// 値関数で振る舞いを定義
func (u User) SetName(name string) {
    u.Name = name
}

// ポインタ関数で振る舞いを定義
func (u *User) SetName2(name string) {
    u.Name = name
}

func main() {
    
    // 値関数の呼び出し
    user := User{ "Yohei" }
    user.SetName("Sasuke")
    fmt.Println("user.Name:", user.Name)    // user.Name: Yohei (関数内の変更は反映されず)

    // ポインタ関数の呼び出し
    user2 := User{ "Yohei" }
    user2.SetName2("Sasuke")
    fmt.Println("user2.Name:", user2.Name)  // user2.Name: Sasuke (関数内の変更が反映された)
}
と、このように関数内での構造体の値の変更が、メソッド呼び出し後にも反映されるか否か、という振る舞いが変化します。そのため、関数内で構造体の値を変更する場合には、ポインターメソッドで定義する必要があります。
この辺、Goを初めて扱った時には分からず混乱しておりました(笑)。

これを使い分けられると、よりGo言語らしいプログラミングができるのかなと思う今日この頃です。



Tips1:ポインタメソッドとnilガード

上記で定義したポインタメソッドですが、実は、以下のようにnilが格納されたポインタからも呼び出すことができます。
// https://play.golang.org/p/Pfh_nsue_j

type User struct {
    Name string
}

// ポインタ関数で振る舞いを定義
func (u *User) Greeting(msg string) {
    fmt.Printf("%s\n", msg)
}


func main() {
    
    var nilUser *User
    
    // ポインタ関数の呼び出し
    nilUser.Greeting("Hello")  // Hello
}
しかし、以下のように関数内で、自分のインスタンスにアクセスしようとするとpanicが発生します。
// https://play.golang.org/p/UI1FC_e3rS
type User struct {
    Name string
}

// ポインタ関数で振る舞いを定義
func (u *User) Greeting(msg string) {
    fmt.Printf("%s from %s\n", msg, u.Name)
}


func main() {
    
    var nilUser *User
    
    // ポインタ関数の呼び出し
    nilUser.Greeting("Hello")  // => panic: runtime error: invalid memory address or nil pointer dereference
}
なので、必要があれば、nilチェックを行います。
// https://play.golang.org/p/9SRGPvVt29
func (u *User) Greeting(msg string) {

    // nilチェックを追加.
    if (u == nil) {
        fmt.Println("user is nil.")
        return
    }

    fmt.Printf("%s from %s\n", msg, u.Name)
}
こうすると、panicにならずに済みます。ただ、nilチェックを入れるかはプロジェクトの設計次第かなーと思います。入れないものも多い気がする。



Tips2:メソッドの色々な呼び出し方

今までの呼び出し方が普通ですが、実は以下のような呼び出し方をすることもできます。
// https://play.golang.org/p/dEPZiOtNJx
type User struct {
    Name string
}

// 値関数で振る舞いを定義
func (u User) Greeting(msg string) {
    fmt.Printf("%s by %s\n", msg, u.Name)
}

// ポインタ関数で振る舞いを定義
func (u *User) Greeting2(msg string) {
    fmt.Printf("%s to %s\n", msg, u.Name)
}


func main() {
    
    u := User{ "Yohei" }
    
    // 同じ
    u.Greeting("Hello")       // Hello by Yohei
    User.Greeting(u, "Hello") // Hello by Yohei
    

    // 同じ
    u.Greeting2("Hello")            // Hello to Yohei
    (*User).Greeting2(&u, "Hello")  // Hello to Yohei
    
    // 同じ
    a := User.Greeting
    b := u.Greeting
    a(u, "Hello")      // Hello by Yohei
    b("Hello")         // Hello by Yohei
}
このような呼び出し方をするのはかなり稀だと思いますが、この辺りの挙動は「コンパイラのソースコードを確かめてみる」で解説されてますので、興味ある方は確認して見てください。



参考URL

今回の内容を学ぶために、以下の記事を参照しました。ありがとうございます。

メソッドのポインタ vs 値 - Effective Go | golang.jp

Go 言語の値レシーバとポインタレシーバ | Step by Step

【Go言語入門】構造体とポインタについて - Qiita



最後に

ポインタの使い方はまだまだ慣れませんが、Go言語は新しい発見がたくさんあって楽しい言語だなと思います。今後も色々と学んでブログにアウトプットしていきたいと思います。

最後になりますが本ブログでは、Go言語・Python・Linux・フロントエンド・Node.js・インフラ・開発環境・Swift・Java・機械学習など雑多に情報発信をしていきます。自分の第2の脳にすべく、情報をブログに貯めています。気になった方は、本ブログのRSSTwitterをフォローして頂けると幸いです ^ ^。

最後までご覧頂きましてありがとうございました!





こんな記事もいかがですか?

RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。