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

「レシーバ」とは

Go 言語はある種のオブジェクト指向プログラミング (OOP) 言語であり、 OOP 言語の慣例通り、メソッドを呼び出される対象のことを「レシーバ」と呼びます。

1
2
3
4
  p := Person{Name: "Taro"}
p.Greet("Hi")

コイツ

ちなみになぜ「レシーバ」と呼ぶのかというと、昔の OOP 言語の文脈ではメソッド呼び出しのことを「メッセージの送信」と言い、メソッドを呼び出される側は「メッセージの受信側」だからです。

「値レシーバ」と「ポインタレシーバ」

Go 言語では「値」と「ポインタ」が明示的に区別されているため、たとえばある構造体に対してメソッドを定義する場合でも、「値型」に対する定義なのか「ポインタ型」に対する定義なのかはっきりと区別しなければなりません。それぞれについて簡単に説明します。

値レシーバ

「値型」に対してメソッド定義されたものが「値レシーバ」です。 Go 言語では構造体は値なので、以下の例では Person という値型に対して Greet() というメソッドを定義しています。

1
2
3
4
5
6
7
8
9
10
11
type Person struct{ Name string }

// Person 型に対してメソッドを定義する
func (p Person) Greet(msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.Name)
}

func main() {
p := Person{Name: "Taro"} // 値型の変数を用意する
p.Greet("Hi") //=> Hi, I'm Taro.
}

ポインタレシーバ

「ポインタ型」に対してメソッド定義されたものが「ポインタレシーバ」です。以下の例では *Person というポインタ型に対して Shout() というメソッドを定義しています。

1
2
3
4
5
6
7
8
9
10
11
type Person struct{ Name string }

// *Person 型に対してメソッドを定義する
func (pp *Person) Shout(msg string) {
fmt.Printf("%s!!!\n", msg)
}

func main() {
pp := &Person{Name: "Taro"} // ポインタ型の変数を用意する
pp.Shout("OH MY GOD") //=> OH MY GOD!!!
}

コンパイラによるレシーバの暗黙的変換

しかし実際には、メソッド呼び出し時にこのあたりのことを意識しなくてもすみます。それは Go 言語仕様の「呼び出し」のセクションにある通り、レシーバの【値型 ⇔ ポインタ型】間の変換はコンパイラが暗黙的におこなってくれるからです。それぞれ例を見てみましょう。

値レシーバの場合

値型に対してあるメソッドが定義されているときに、ポインタ型変数からそのメソッドを呼び出そうとすると、コンパイラが暗黙的に値型のメソッド呼び出しに変換してくれます。以下の例では、 値型 PersonGreet() が定義されていますが、 *Person 型変数 pp からでも問題なく呼び出せます。

1
2
3
4
5
6
7
8
9
10
11
12
type Person struct{ Name string }

// Person 型に対してメソッドを定義する
func (p Person) Greet(msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.Name)
}

func main() {
pp := &Person{Name: "Taro"} // ポインタ型の変数を用意する
(*pp).Greet("Hi") //=> Hi, I'm Taro. | 当然呼び出せる
pp.Greet("Hi") //=> Hi, I'm Taro. | コンパイラが上のコードに変換してくれる
}

したがって、以下のように nil ポインタ変数から呼び出そうとすると panic を起こします。 *nilp が存在しないからです。

1
2
var nilp *Person // nil ポインタ変数だと...
nilp.Greet("Hi") //=> panic: runtime error: invalid memory address or nil pointer dereference

これは Greet() の中で p.Name を使っていることとは関係なく発生します。

上記コードの Playground: http://play.golang.org/p/SZOv0hTicF

ポインタレシーバの場合

一方、ポインタ型に対してあるメソッドが定義されているときに、値型変数からそのメソッドを呼び出そうとすると、コンパイラが暗黙的にポインタ型のメソッド呼び出しに変換してくれます。以下の例では、 ポインタ型 *PersonShout() が定義されていますが、 Person 型変数 p からでも問題なく呼び出せます。

1
2
3
4
5
6
7
8
9
10
11
12
type Person struct{ Name string }

// *Person 型に対してメソッドを定義する
func (pp *Person) Shout(msg string) {
fmt.Printf("%s!!!\n", msg)
}

func main() {
p := Person{Name: "Taro"} // 値型の変数を用意する
(&p).Shout("OH MY GOD") //=> OH MY GOD!!! | 当然呼び出せる
p.Shout("OH MY GOD") //=> OH MY GOD!!! | コンパイラが上のコードに変換してくれる
}

上記コードの Playground: http://play.golang.org/p/Vs-LOJq_1d

さらに、ポインタレシーバのメソッドは nil ポインタ変数からでも呼び出しが可能です。

1
2
var nilp *Person        // nil ポインタ変数でも...
nilp.Shout("OH MY GOD") //=> OH MY GOD!!! | ちゃんと呼び出せる

ただし当然のことながら、メソッド内でフィールドを使っていたら参照先がないので panic になります。以下の例では、 p.Name を使っているメソッド ShoutNamenil ポインタ変数から呼び出すと、メソッド呼び出しそのものは正常におこなわれるものの、フィールド呼び出しがあるせいで panic になります。

1
2
3
4
5
6
7
8
9
10
11
type Person struct{ Name string }

// *Person 型に対してメソッドを定義する
func (p *Person) ShoutName() {
fmt.Printf("I'm %s!!!\n", p.Name) // p.Name フィールドを呼び出していると...
}

func main() {
var nilp *Person // nil ポインタだと...
nilp.ShoutName() //=> panic: runtime error: invalid memory address or nil pointer dereference
}

上記コードの Playground: http://play.golang.org/p/UzB32jpi2q

メソッド定義とメソッド呼び出しの真実

メソッド定義の真実

メソッド定義は本質的には関数定義と等価です。 Go 言語の場合

1
2
3
func (p Person) Greet(msg string) {
// ...
}

というメソッド定義は、内部的には

1
2
3
func Person.Greet(p Person, msg string) {
// ...
}

という「メソッド式 (method expression)」と呼ばれる関数として定義されます。これは値レシーバの場合で、同様にポインタレシーバの場合

1
2
3
func (pp *Person) Shout(msg string) {
// ...
}

1
2
3
func (*Person).Shout(pp *Person, msg string) {
// ...
}

という関数として定義されます。

コンパイラのソースコードを確かめる

Go 1.5.2 のソースコードを基にこのことを確かめてみましょう。該当箇所は以下になります (コメントはすべて僕が追記したものです)。

src/cmd/compile/internal/gc/dcl.go#L1325-L1350

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// n: メソッドを表す AST ノード
// t: メソッドをもつ型を表す AST ノード
func methodname1(n *Node, t *Node) *Node {
star := ""
if t.Op == OIND {
star = "*"
t = t.Left
}

if t.Sym == nil || isblank(n) {
return newfuncname(n.Sym)
}

var p string
if star != "" {
p = fmt.Sprintf("(%s%v).%v", star, t.Sym, n.Sym) // (*Type).Method
} else {
p = fmt.Sprintf("%v.%v", t.Sym, n.Sym) // Type.Method
}

if exportname(t.Sym.Name) {
n = newfuncname(Lookup(p))
} else {
n = newfuncname(Pkglookup(p, t.Sym.Pkg))
}

return n
}

上記コードの変数 p を見ていただくとわかる通り、メソッド定義においてメソッド名は (*Type).Method または Type.Method という名前をもつ、関数の AST ノードに変換されています。

メソッド呼び出しの真実

メソッド定義と同様に、メソッド呼び出しもメソッド式という関数の呼び出しの糖衣構文にすぎません。たとえば、以下の例では p.Greet(...)Person.Greet(p, ...)pp.Shout(...)(*Person).Shout(pp, ...) はそれぞれ等価です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Person struct{ Name string }

func (p Person) Greet(msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.Name)
}

func (p *Person) Shout(msg string) {
fmt.Printf("%s!!!\n", msg)
}

func main() {
p := Person{Name: "Taro"}
// 以下は等価
p.Greet("Hi") //=> Hi, I'm Taro.
Person.Greet(p, "Hi") //=> Hi, I'm Taro.

pp := &Person{Name: "Taro"}
// 以下は等価
pp.Shout("OH MY GOD") //=> OH MY GOD!!!
(*Person).Shout(pp, "OH MY GOD") //=> OH MY GOD!!!
}

上記コードの Playground: http://play.golang.org/p/3ENNQWghCX

ちなみに、メソッド式やメソッドは値としても扱えるので、以下のように変数に代入して利用することもできます。この場合、メソッド式 f にはレシーバ引数が必要で、メソッド値 g には必要ないことに注意してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Person struct{ Name string }

func (p Person) Greet(msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.Name)
}

func main() {
p := Person{Name: "Taro"}
f := Person.Greet
g := p.Greet

f(p, "Hello") //=> Hello, I'm Taro.
g("Hello") //=> Hello, I'm Taro.
}

上記コードの Playground: http://play.golang.org/p/kpaGwzPu1u

Go は関数が第一級オブジェクトなので便利ですね。

メソッド定義で意識すべきこと

改めて強調すると、メソッド定義において意識すべきなのは、レシーバが「第0引数」であるということです。たとえば、以下のメソッド定義

1
2
3
4
5
6
7
8
9
type Person struct{ Name string }

func (p Person) Greet(msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.Name)
}

func (pp *Person) Shout(msg string) {
fmt.Printf("%s!!!\n", msg)
}

は、内部的には

1
2
3
4
5
6
7
func Person.Greet(p Person, msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.Name)
}

func (*Person).Shout(pp *Person, msg string) {
fmt.Printf("%s!!!\n", msg)
}

というメソッド式(関数)として定義されます。ただし直接このような関数名での関数定義はできません(コンパイラが . を含むシンボル名を許していないので)。

そして、メソッド呼び出しがメソッド式呼び出しと等価ということは、メソッド定義をする際には以下のことに気をつける必要があります。

1. メソッド呼び出しごとにレシーバの値はコピーされる

関数呼び出しごとに引数の値はスタックにコピーされますので、当然メソッド呼び出し時にはレシーバの値もコピーされます。

1
2
3
p := Person{Name: "Taro"}
Person.Greet(p, "Hi") // p の値はコピーされてメソッド内で使われます
p.Greet("Hi") // p の値はコピーされてメソッド内で使われます

したがって、特にデータ量の大きな構造体に値レシーバのメソッドを定義すると、メソッド呼び出しごとにコピーが発生するので非常に非効率であることがわかります。このことから、構造体におけるメソッド定義は原則ポインタレシーバに対しておこなったほうがよいです。

2. 値レシーバの値はメソッド内で書き換えても元のレシーバの値にはまったく影響がない

値レシーバの場合、値そのものがまるっとコピーされるので、メソッド内でいくら値を書き換えても元のレシーバの値にはまったく影響がありません。

以下の例の場合、 UnchangeName() の処理は一見 setter っぽくレシーバの name を書き換えているように見えますが、 p が値レシーバなので p.Greet() で表示される名前は Jiro ではなく Taro のままになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Person struct{ name string }

func (p Person) Greet(msg string) {
fmt.Printf("%s, I'm %s.\n", msg, p.name)
}

func (p Person) UnchangeName(name string) {
p.name = name
}

func main() {
p := Person{name: "Taro"}
// p の値はコピーされるので、フィールドを書き換えても影響ない
Person.UnchangeName(p, "Jiro")
p.UnchangeName("Jiro")

p.Greet("Hi") //=> Hi, I'm Taro.
}

上記コードの Playground: http://play.golang.org/p/5kJVlx_XY7

このことを利用すると、不変オブジェクトをつくることができます。その典型例が time パッケージの Time です。

time.Time は不変オブジェクト

time.Time の API 一覧を見てもらうとわかる通り、 Time オブジェクトは

  • フィールドがすべてプライベート
  • レシーバは Unmarshal 系を除きすべて Time 型(値型)
  • オブジェクトを返すメソッドの戻り値もすべて Time 型(値型)

となっており、 *Time 型(ポインタ型)は基本的に現れません。

フィールドがプライベートなので直接の値の変更ができず、またレシーバも戻り値もほぼすべてポインタ型ではなく値型なので、一度生成された Time オブジェクトは一切変更する手段がないということになります。

Java では java.util.Date クラスや java.util.Calendar クラスが可変でスレッドセーフではないことが長年問題となっており、やっと Java 8 で不変オブジェクトを基本とする java.time パッケージが導入されました(Java SE 8 Date and Time -新しい日付/時間ライブラリが必要になる理由-)。 Go では少なくともこの種の問題は避けられているわけですね。

それでも Go ではエクスポートされたフィールドの直接書き換えや、オブジェクトの内部状態を書き換える副作用のあるメソッドが気軽に用いられる傾向にあります。並行処理に耐える不変オブジェクトを設計したい場合は、 Time を参考にした構造体設計にするとよいかもしれません。

3. ポインタレシーバにするとメソッド内でレシーバの値を書き換えられる

一方、レシーバをポインタレシーバにした場合、メソッド呼び出し時にコピーされるのもポインタになるので、そのポインタを使うことでレシーバの実際の値を書き換えることができます(ただし mapchan などの参照型をベースとする型は半分ポインタみたいなものなので、値レシーバでも可能ですが)。

たとえば以下の例の場合、 ChangeName() メソッドのレシーバは *Person なので、 p.name フィールドを書き換えることが可能であり、 p.Greet() で表示される名前は Jiro に変化します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct{ name string }

func NewPerson(name string) *Person {
return &Person{name: name}
}

// p の値を変更したいのでポインタレシーバで定義する
func (p *Person) ChangeName(name string) {
p.name = name
}

func main() {
p := NewPerson("Taro")
p.ChangeName("Jiro")

p.Greet("Hi") //=> Hi, I'm Jiro.
}

上記コードの Playground: http://play.golang.org/p/asH9DZRmxG

逆に言うと、レシーバの内部状態を変更したいメソッドは、(参照型を除き)必ずポインタレシーバで定義しなければなりません

4. ポインタレシーバのメソッド内でフィールドを呼び出す場合には、その前に nil チェックをすべき

ポインタレシーバのメソッドは nil ポインタ変数からでも呼び出しが可能なため、ポインタレシーバのメソッド内でフィールドを呼び出している場合、常に nil pointer dereferencepanic になる危険性を孕んでいます。

以下の例の場合、 ChangeNameUnsafe() には nil チェックがないため panic になりますが、 ChangeNameSafe() のほうは nil チェックをしているため安全です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Person struct{ name string }

func (p *Person) ChangeNameUnsafe(name string) {
// これだけだと危険
p.name = name
}

func (p *Person) ChangeNameSafe(name string) {
// 他の言語だと this == nil みたいなものだから変な感じがするが、
// きちんとガード句を設けておけば安全
if p == nil {
return
}
p.name = name
}

func main() {
var pp *Person // nil ポインタ変数だと...
pp.ChangeNameUnsafe("Jiro") //=> panic: runtime error: invalid memory address or nil pointer dereference
pp.ChangeNameSafe("Jiro") // こちらは安全
}

レシーバは他の言語でいう thisself などに相当するため、メソッド内で nil チェックをすることには違和感があるかもしれません。しかし「レシーバは第0引数」であることを思い出せば、他のポインタ引数の nil チェックと同様に考えて、安全なプログラムを書くためにきちんとやったほうがよいです(僕もよく忘れますが)。

逆に考えると、 Java などと違いメソッド内でレシーバが nil か否かに応じたハンドリングができるので、それがきちんとなされていれば、メソッド呼び出し側が事前の nil チェックをする必要がないありがたさもあります。

レシーバの値を使わない場合はレシーバ変数を記述しなくてもよい

上記のことに関連して、実はメソッド定義におけるレシーバ変数は書かなくても問題なくコンパイルできるため、メソッド内でレシーバの値を使わない場合は書かないのも1つの手です。

1
2
3
4
5
6
7
type Person struct{ name string }

// p の値を使わないときは書かない
// ↓
func (*Person) Shout(msg string) {
fmt.Printf("%s!!!\n", msg)
}

そのような処理は本質的にはメソッドにする必要がなく、単なる関数のままでもよいため、このようなケースはあまり多くないとは思います。ただ、パッとシグニチャを見ただけでレシーバの値が使われていないことがわかるので、ドキュメントとしての価値もあると思います。

まとめ:値レシーバとポインタレシーバの使い分け

最後に、値レシーバとポインタレシーバの使い分けについて整理しておきたいと思います。

基本はポインタレシーバ

レシーバの値を変更したい場合は(参照型を除き)必ずポインタレシーバにしなければなりません。構造体も基本的にはポインタレシーバにしたほうがよいでしょう。

また、統一性の観点からも、ある型のメソッドのレシーバを1つでもポインタレシーバにした場合には、値の変更の有無にかかわらずすべてポインタレシーバにしたほうがよいと思います。

値レシーバにしたほうがよい場合

レシーバの値を変更する必要がなく、かつ以下のどれかに該当するときは値レシーバにしたほうがよいです。ただし参照型の場合は値の変更の有無は関係ありません。

int, string などのプリミティブ型をベースとする型

プリミティブ型はコピーコストが小さいため、通常は値レシーバにします。

map, chan といった参照型をベースとする型

参照型は半分ポインタみたいなものなので、値レシーバのままでも保持する要素の値を変更できます。コピーコストも大きくないため、通常は値レシーバにします。

不変型

先に例として出した time.Time のような不変型を定義したい場合には、すべて値レシーバとして定義するのも良い方法です。

小さい構造体

コピーコストの小さい構造体の場合には、値レシーバでメソッドを定義すると処理がスタックで完結するので、ヒープへのアロケート回数や GC の回数が減ることが期待できます。

ただしこの使い分けは感覚的におこなうのではなく、本当に効率的になるのかきちんと計測した結果に基いておこなうべきです。 Go 言語ではベンチマークの計測や pprof などによる解析が容易におこなえるので、これらを活用するとよいでしょう。