CLOVER🍀

That was when it all began.

Goのメソッドのレシーバータイプ(値 or ポインタ)について調べてみる

これは、なにをしたくて書いたもの?

Goの勉強をしていて、メソッドのレシーバー定義が2つあって、どういう使い分けをしたらいいのかよくわからなかったので
調べてみることにしました。

2つ、というのは、こういうのと

func (p Point) Length() float64 {

こういうのですね。

func (p *Point) Length() float64 {

レシーバーの型に*がついているかどうか、というのが違いになります。

用語の整理

メソッド、レシーバーという言葉の意味を、最初にかいておきましょう。

Goのドキュメントの、こちらを参照します。

The Go Programming Language Specification / Method declarations

メソッドとは、レシーバーを持つ関数のことです。

A method is a function with a receiver.

そしてレシーバーは、メソッドの定義において、メソッド名の前にパラメーターとして定義されるものです。

構文としては、以下のようになっています。

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

レシーバーの型は、型そのもの、もしくはポインタ型である必要があります。

つまり、最初に書いた以下の2つの違いは、レシーバーの型が型そのものなのか、レシーバー型なのかという違いになります。

// 型そのもの
func (p Point) Length() float64 {


// ポインタ型
func (p *Point) Length() float64 {

*がついている方が、ポインタ型です。
それぞれ、値レシーバー、ポインタレシーバーと呼ぶそうです。

この2つの定義方法がどう違うかを理解しようというのが、今回の話です。

ちなみに、メソッドの宣言がどちらであろうとも、利用する時には意識しなくてもよさそうです。以下のように変換が入って
いるのだと。

If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m():

The Go Programming Language Specification / Calls

結論

結論を先に書くと、以下を見るのがよいでしょう。

Go Code Review Comments / Receiver Type

ところで「値レシーバー」、「ポインタレシーバー」というのは、以下のように登場してこの文書内で続くので、ここでは
以降「値レシーバー」、「ポインタレシーバー」という言葉を使っていきましょう。

Choosing whether to use a value or pointer receiver on methods can be difficult, especially to new Go programmers.

以下のように選ぶとよいみたいです。

  • 値レシーバーを選ぶ場合
    • レシーバーがmap、func、chanの場合
    • レシーバーがスライスであり、メソッドがスライスを再度スライスを行う、またはスライスの再割当てをしない場合
    • レシーバーが可変フィールドやポインタを持たない場合
    • レシーバーが小さな配列の場合
    • レシーバーがtime.Timeのような値型の構造体の場合
    • intやstringのような単純な基本型の場合
  • ポインタレシーバーを選ぶ場合
    • メソッドがレシーバーの内容を変更する場合
    • レシーバーがsync.Mutexまたは同様の同期フィールドを含む構造体の場合
      • コピーされた同期フィールドを利用してしまうことを避けるため
    • レシーバーが大きな構造体または配列の場合
      • ポインターレシーバーの方が効率的
      • 基準は、メソッドの引数に要素を並べた時に「多い」と感じる場合はポインタレシーバーを選ぶ
    • レシーバーの型が構造体、配列、スライスであり、その要素のいずれかが変更される可能性がある場合
      • 意図を明確にするため
    • どちらを選んだらよいか疑わしい場合

最後の項目がそのものズバリですが、上記の内容を見てそれでも迷うようならポインタレシーバーを使え、のようです。

値レシーバーの場合、メソッド呼び出し時にレシーバーのコピーが作成されるため、メソッド内でレシーバーの内容を変更する
ケースには対応できません。なので、この場合はポインタレシーバーを使う必要があります。

一方で、値レシーバーで定義されたメソッドに値を渡す場合は、レシーバーのコピーをヒープに割り当てる代わりにスタック上のコピーを
利用できるためガベージの量を減らせる可能性がある、と。が、これは常に成功するとは限らないそうな…。
この最適化を狙って値レシーバーを選択するのは誤りのようで、プロファイリングしてから選んでね、だそうです。

ちょっと試す

と、いろいろ調べてみたところで、自分でも少し試してみましょう。

お題は以下とします。

  • 構造体を作成
  • 同じ処理を行うメソッドを、値レシーバーとポインタレシーバーの2パターン作成し、動作を確認する
    • 処理としてはレシーバーの値を参照するだけのもの、レシーバーの値を変更するものを用意する
  • メソッド呼び出しに使うレシーバーの方も、型そのものとポインタ型の2つを用意し、メソッドのバリエーションと組み合わせる

環境

今回の環境は、こちら。

$ go version
go version go1.15.6 linux/amd64

確認用のモジュールを作成。

$ go mod init method-receiver-sample
go: creating new go.mod: module method-receiver-sample

動作確認は、testifiy assertを使って行うことにしましょう。
go.mod

module method-receiver-sample

go 1.15

require github.com/stretchr/testify v1.6.1

go.sum

github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

構造体の定義とテストコードの雛形

まずは、利用する構造体を定義します。姓名、年齢、身長をフィールドに持った、人をお題にした構造体にしましょう。 person.go

package main

import (
    "fmt"
)

type Person struct {
    firstName string
    lastName  string
    age       int
    height    int
}

// ここにメソッドを定義する

この構造体に対するメソッドを定義していきます。

また、テストコードの雛形は以下のように用意します。
person_test.go

package main

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

// ここにテストを書く

レシーバーの値を参照するだけのケース

最初に、レシーバーの値を参照するだけのケースを書いてみます。

姓と名を結合させましょう。最初が値レシーバー、2つ目がポインタレシーバーですね。

// value receiver
func (p Person) GetName() string {
    return fmt.Sprintf("%s %s", p.firstName, p.lastName)
}

// pointer receiver
func (p *Person) GetNamePointer() string {
    return fmt.Sprintf("%s %s", p.firstName, p.lastName)
}

では、テストを書いてみます。最初に構造体を初期化して、値レシーバー、ポインタレシーバーそれぞれのメソッドを呼び出しています。
後半は構造体のポインタを得ています。

func TestGetName(t *testing.T) {
    // value
    var person Person = Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    assert.Equal(t, "カツオ 磯野", person.GetName())        // value receiver method
    assert.Equal(t, "カツオ 磯野", person.GetNamePointer()) // pointer receiver method

    // pointer
    var personAsPointer *Person = new(Person)
    personAsPointer.firstName = "カツオ"
    personAsPointer.lastName = "磯野"
    personAsPointer.age = 11
    personAsPointer.height = 143

    assert.Equal(t, "カツオ 磯野", personAsPointer.GetName())        // value receiver method
    assert.Equal(t, "カツオ 磯野", personAsPointer.GetNamePointer()) // pointer receiver method
}

newというのは、指定された型の領域を確保し、そのポインタを返す組み込み関数です。

The Go Programming Language Specification / Allocation

テストコードを見ればわかりますが、どの組み合わせも同じ結果になります。

また、構造体を扱う型が型そのものであっても、ポインタ型であっても、メソッドのレシーバーの型がどちらであっても
問題なく呼び出せることが確認できました。

レシーバーの値を変更するケース

次に、レシーバーの値を変更してみます。

年齢と身長を加算するメソッドを書いてみます。

// value receiver
func (p Person) Grow(age int, height int) {
    p.age += age
    p.height += height
}

// pointer receiver
func (p *Person) GrowPointer(age int, height int) {
    p.age += age
    p.height += height
}

テストコードはこちら。磯野カツオ氏に、2年で10cm伸びてもらいました。

func TestGrowing(t *testing.T) {
    // value
    var person1 Person = Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    person1.Grow(2, 10)                  // value receiver method
    assert.Equal(t, 11, person1.age)     // no change
    assert.Equal(t, 143, person1.height) // no change

    var person2 Person = Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    person2.GrowPointer(2, 10)           // pointer receiver method
    assert.Equal(t, 13, person2.age)     // change
    assert.Equal(t, 153, person2.height) // change

    // pointer
    var personAsPointer1 *Person = &Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    personAsPointer1.Grow(2, 10)                  // value receiver method
    assert.Equal(t, 11, personAsPointer1.age)     // no change
    assert.Equal(t, 143, personAsPointer1.height) // no change

    var personAsPointer2 *Person = &Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    personAsPointer2.GrowPointer(2, 10)           // value receiver method
    assert.Equal(t, 13, personAsPointer2.age)     // change
    assert.Equal(t, 153, personAsPointer2.height) // change
}

今回、変数の型は明示するようにしています。

ポインタ型の初期化方法は、&演算子を使うように変更しました(フィールドに値を指定するコードが長くなるので)。

レシーバーを型そのもので扱おうと、ポインタ型で扱おうと、メソッドがレシーバーの値を変更する場合はポインタレシーバーで
ある必要があるようです。値レシーバーで宣言したメソッドを使った方には、レシーバーに対する変更が反映されていません。

というか、どちらの型で変数宣言しても、メソッドがポインタレシーバーになっていれば変更できるということですね。

このあたりが以下で述べられていたことなのでしょう。

If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m():

The Go Programming Language Specification / Calls

概ね、確認したい挙動は見れた感じです。

オマケ

ちなみに、レシーバーの値をまったく触らない場合は、レシーバーを扱うための変数は省略できるようです。

func (*Person) SayHello() {
    fmt.Println("Hello World!!")
}
func TestSayHello(t *testing.T) {
    var person Person = Person{}
    person.SayHello()

    var personAsPointer *Person = new(Person)
    personAsPointer.SayHello()
}

まとめ

Goのメソッドの宣言方法における、値レシーバーとポインタレシーバーの違いがよくわからなかったので、この機会に
整理できたかなと思います。

今後は、ちゃんと意識して使っていきましょう。

最後に、今回作成したコードの全体を載せておきます。
person.go

package main

import (
    "fmt"
)

type Person struct {
    firstName string
    lastName  string
    age       int
    height    int
}

// value receiver
func (p Person) GetName() string {
    return fmt.Sprintf("%s %s", p.firstName, p.lastName)
}

// pointer receiver
func (p *Person) GetNamePointer() string {
    return fmt.Sprintf("%s %s", p.firstName, p.lastName)
}

// value receiver
func (p Person) Grow(age int, height int) {
    p.age += age
    p.height += height
}

// pointer receiver
func (p *Person) GrowPointer(age int, height int) {
    p.age += age
    p.height += height
}

func (*Person) SayHello() {
    fmt.Println("Hello World!!")
}

person_test.go

package main

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

func TestGetName(t *testing.T) {
    // value
    var person Person = Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    assert.Equal(t, "カツオ 磯野", person.GetName())        // value receiver method
    assert.Equal(t, "カツオ 磯野", person.GetNamePointer()) // pointer receiver method

    // pointer
    var personAsPointer *Person = new(Person)
    personAsPointer.firstName = "カツオ"
    personAsPointer.lastName = "磯野"
    personAsPointer.age = 11
    personAsPointer.height = 143

    assert.Equal(t, "カツオ 磯野", personAsPointer.GetName())        // value receiver method
    assert.Equal(t, "カツオ 磯野", personAsPointer.GetNamePointer()) // pointer receiver method
}

func TestGrowing(t *testing.T) {
    // value
    var person1 Person = Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    person1.Grow(2, 10)                  // value receiver method
    assert.Equal(t, 11, person1.age)     // no change
    assert.Equal(t, 143, person1.height) // no change

    var person2 Person = Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    person2.GrowPointer(2, 10)           // pointer receiver method
    assert.Equal(t, 13, person2.age)     // change
    assert.Equal(t, 153, person2.height) // change

    // pointer
    var personAsPointer1 *Person = &Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    personAsPointer1.Grow(2, 10)                  // value receiver method
    assert.Equal(t, 11, personAsPointer1.age)     // no change
    assert.Equal(t, 143, personAsPointer1.height) // no change

    var personAsPointer2 *Person = &Person{firstName: "カツオ", lastName: "磯野", age: 11, height: 143}

    personAsPointer2.GrowPointer(2, 10)           // value receiver method
    assert.Equal(t, 13, personAsPointer2.age)     // change
    assert.Equal(t, 153, personAsPointer2.height) // change
}

func TestSayHello(t *testing.T) {
    var person Person = Person{}
    person.SayHello()

    var personAsPointer *Person = new(Person)
    personAsPointer.SayHello()
}

参考

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

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