これは、なにをしたくて書いたもの?
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() }