Go

オブジェクト指向言語に慣れたプログラマが、Goのメソッドでレシーバーを書く時に気をつけたいこと

More than 1 year has passed since last update.

Goのメソッドは、構造体に関数を定義できる機能だ。
だが、Java、PHPやJavaScriptのプログラマが大いに気をつけるべきポイントがある。
それが「レシーバー」の扱いだ。

例えば以下のようなJavaScriptのコードを見てほしい。

var obj = {}
obj.setName = function(name){
    this.name = name;
}

obj.getName = function(){
    return this.name;
}

obj.setName("Taro");
console.log( "Name => " + obj.getName() ); // Name => Taro

obj にSetter関数とGetter関数を用意して、Setterに "Taro" を入れて呼び出す。すると自分自身の name プロパティに名前がセットされ、Getterで取得すると、"Taro" が戻ってくる。特に不思議なことはないだろう。

Goで書いてみる

では同じことを Go で書いてみる

package main

import "fmt"

type Obj struct {
    Name string
}

func (o Obj) SetName(name string) {
    o.Name = name
}

func (o Obj) GetName() string {
    return o.Name
}

func main() {
    o := Obj{}
    o.SetName("Hanako")
    fmt.Println("name => " + o.GetName()) // name => [空]  (!!!)
}

最後の行を見てほしい。
なんと、o.SetName("Taro")したにもかかわらず、o.GetName() の結果が「空」になってしまっている。

なぜなのか。

原因はレシーバーの書き方にあった

もう一度レシーバーの定義を見てみよう。

func (o Obj) SetName(name string)
func (o Obj) GetName() string

レシーバーを (o Obj) と書いているので、Obj型の定義になっている。

ところがだ。実はGoではメソッド呼出時に "レシーバーそのものがコピーされる" という仕様があり、o.SetName("Hanako") と呼出たときに、メソッドの中に渡される o という変数は、呼出元の o とは実態が異なるのである。コピーされた o のプロパティを変えているのだから、その次の行で o.GetName したときに、値が空のままなのは当たり前なのである。

どうすればよいのか

結論から言えば、レシーバーの型をポインタ型で書けばこのような問題は起こらない。(o Obj) の部分を (o *Obj) にすれば、ポインターのコピーになるので実態は同じになるのである。

さきほどのコードを、以下のように書き直してみよう。

func (o *Obj) SetName2(name string) {
    o.Name = name
}

func (o *Obj) GetName2() string {
    return o.Name
}

func main() {
    o := Obj{}
    o.SetName2("Hanako")
    fmt.Println("name => " + o.GetName2()) // name => Hanako
}

無事、SetNameした値が取得できた。

一般的なオブジェクト指向言語から来た人間にとっては、あまり想定もしない挙動だと思う。レシーバーは便利な表現だけに、気をつけたいところである。