【Go言語】構造体におけるポインタメソッドの使いどころ

Go言語

Go言語ではレシーバを引数にとることで型のメソッドを定義できます。1
Go言語のレシーバには『変数レシーバ』と『ポインタレシーバ』の2種類があります。
変数レシーバは値レシーバと呼ぶこともありますが、A Tour of Go#Pointer receiversに従い、変数レシーバと表現します。

この記事では、変数レシーバを利用したメソッドを『変数メソッド』、ポインタレシーバを利用したメソッドを『ポインタメソッド』と呼ぶことにします。

今回は構造体のメソッドを作成する際、どういった時にポインタを使うべきか(= ポインタメソッドにするべきか)について紹介します。

構造体のポイントメソッドを学習する前の予備知識

構造体のポインタメソッドについて学習する前に理解しておくべき内容について紹介します。

変数メソッドとポインタメソッドについて

変数メソッドはコピーしたレシーバを利用してメソッドを実行します。
構造体の変数メソッドであれば、構造体の中身がコピーされてメソッドの中で利用されます。
コピーを生成してメソッドを実行するため、変数メソッド内部でおきた変更はレシーバに影響しません。

ポインタメソッドは受け取ったポインタを利用してメソッドを実行します。
変数メソッドを実行するにはレシーバの値をコピーする必要がありますが、ポインタメソッドを実行する場合はアドレスを受け取るだけで済みます。

変数メソッドおよびポインタメソッド呼び出し時のGo言語の仕様

Go言語ではメソッド呼び出し時のレシーバをよしなに解釈してくれます。

ポインタメソッドを呼び出す際のレシーバが変数vだった場合、Go言語は&vと解釈します。2
つまり、変数でもポインタでもポインタメソッドの呼び出しが可能です。

package main

import (
  "fmt"
)

type User struct {
  name string
  age  int
}

func (u *User) ChangeName(name string) {
  u.name = name
}

func main() {
  user_1 := &User{name: "Suzuki", age: 20}
  user_1.ChangeName("Tanaka")
  fmt.Println(user_1.name) // Tanaka

  // 変数 user_2 でも &user_2 と解釈されてポインタメソッドが呼び出せる
  user_2 := User{name: "Bob", age: 21}
  user_2.ChangeName("Mike")
  fmt.Println(user_2.name) // Mike
}

逆に、変数メソッドを呼び出す際のレシーバがポインタpだった場合、Go言語は*pと解釈します。3
つまり、変数でもポインタでも変数メソッドの呼び出しが可能です。

package main

import (
  "fmt"
)

type User struct {
  name string
  age  int
}

func (u User) SayName() {
  fmt.Println("My name is " + u.name)
}

func main() {
  // ポインタ型 user_1 でも *user_1 と解釈されて変数メソッドが呼び出せる
  user_1 := &User{name: "Suzuki", age: 20}
  user_1.SayName() // My name is Suzuki

  user_2 := User{name: "Bob", age: 21}
  user_2.SayName() // My name is Bob
}

変数メソッドにするかポインタメソッドにするかの判断基準

Go Code Review Comments#Receiver Typeでは、変数メソッドとポインタメソッドの使い分けについて、以下のようなガイドラインを設けています。

  • ポインタを使うべきではない
    • レシーバが map、func、チャネルの場合
    • レシーバがスライスで、メソッドがスライスを作りなおさない場合
  • ポインタでなければいけない
    • メソッドが値を変更する必要がある場合
    • レシーバが sync.Mutex か、似たような同期するフィールドを持つ構造体の場合
  • ポインタのほうがよい
    • レシーバが大きな構造体や配列の場合
    • 該当するケースが分からず迷っている場合

構造体のメソッドでのポインタのつかいどころ

ここまでの話を踏まえると、構造体のメソッドにおけるポインタのつかいどころは主に2つのパターンがあります。4

構造体におけるポインタメソッドの使いどころ
  • レシーバである構造体のサイズが大きいとき
  • メソッド内で構造体の内容を変更するとき

レシーバである構造体のサイズが大きいとき

変数メソッドはコピーしたレシーバを利用するという性質上、構造体のサイズが大きいほどコピーのコストがかかります。
一方、ポインタメソッドであればアドレスを渡すだけでメソッドが実行できます。

変数メソッドでもロジック上は問題ないのですが、ポインタメソッドにしたほうがパフォーマンスの向上を期待できます。

メソッド内で構造体の内容を変更するとき

前述の『レシーバである構造体のサイズが大きいとき』とは違い、この場合は必ずポインタメソッドでなければいけません。

以下の結果からわかる通り、ポインタメソッドにしないとメソッド内で実行した結果をレシーバである構造体へ反映できません。

package main

import (
  "fmt"
)

type User struct {
  name string
  age  int
}

// NG: 変数メソッド内で変更した内容は元の構造体に反映されない
func (u User) ChangeNameWithValue(name string) {
  fmt.Println("変数メソッド内でのname: " + name)
  u.name = name
}

// OK
func (u *User) ChangeNameWithPointer(name string) {
  fmt.Println("ポインタメソッド内でのname: " + name)
  u.name = name
}

func main() {
  user := User{name: "Suzuki", age: 20}
  fmt.Println("元のname: " + user.name)

  user.ChangeNameWithValue("Tanaka")
  fmt.Println("変数メソッド実行後のname: " + user.name)

  user.ChangeNameWithPointer("Tanaka")
  fmt.Println("ポインタメソッド実行後のname: " + user.name)
}

### 実行結果

元のname: Suzuki
変数メソッド内でのname: Tanaka
変数メソッド実行後のname: Suzuki (← Tanakaにならない!)
ポインタメソッド内でのname: Tanaka
ポインタメソッド実行後のname: Tanaka

たとえば、設定ファイルを読み込んで構造体に値をセットするようなメソッドではポインタメソッドが利用されます。

まとめ

構造体のメソッドにおけるポインタの使いどころ
  • 構造体のサイズが大きいとき
  • メソッド内で構造体の内容を変更するとき

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考