Go は最近のプログラミング言語にしては珍しくポインタを扱えるプログラミング言語。とはいってもC言語よりも簡単で、オブジェクトの初期化やメソッドの定義以外の場所ではおおよそポインタを使っている様には見えない。メソッドやフィールドへのアクセスも .
で出来るし Duck Type によりインタフェースを満たしていれば実体であろうとポインタであろうとそれほど意識する必要はない。ところがこの便利さに乗っかってしまうと思わぬ所で足をすくわれてしまう。
package main
type foo struct {
v int
}
func (f foo) add(v int) {
f.v = v
}
func main() {
var a foo
a.add(3)
println(a.v)
}
このコードは 0 が表示される。メソッドを呼び出す際にはレシーバのオブジェクトを得る必要があるが、foo は実体なのでコピーが生成される。例えばレシーバ f が引数であったと考えると理解しやすくなる。
package main
type foo struct {
v int
}
func add(f foo, v int) {
f.v += v
}
func main() {
var a foo
add(a, 3)
println(a.v)
}
メソッドとレシーバの関係は、実は「単なる関数と第一引数」と考えると分かりやすい。特にC言語でオブジェクト指向をやる様な人達は訓練されているので、Go をやる上でもこの辺りの動作を意識せず扱えているのかもしれない。
訓練されたC言語使いは第一引数が見えないって爺ちゃんが言ってた。
— mattn@従来型IT人材 (@mattn_jp) April 11, 2019
実体のレシーバにメソッドを生やすメリットが無い訳ではない。例えばオブジェクトの値を明示的に変更させたくない場合がそれで、そういった設計にしたい場合は戻り値を使う。
package main
type foo struct {
v int
}
func (f foo) add(v int) foo {
f.v += v
return f
}
func main() {
var a foo
a = a.add(3)
println(a.v)
}
これとは別に、ポインタと実体をまぜて考えてしまうと失敗してしまう事もある。
そして、これを使って
— Akira Ishino (@akrisn) May 14, 2019
func main() {
b := []func(){}
c := []a{{"a"}, {"b"}}
for _, i := range c {
b = append(b, i.f())
}
for _, j := range b {
j()
}
}
この例のポイントは「for range の反復変数はループ毎に新しいコピーが作成されない」という事。つまり上書きになる。プログラマは一見このループが実行されると以下の様になると考えてしまう。
b = append(b, c[0].f())
b = append(b, c[1].f())
だが実際はこう。
var i a
i = c[0]
b = append(b, i.f())
i = c[1]
b = append(b, i.f())
slice の b は一見、c の情報が詰め込まれている様に見えるが、実際は i の情報が詰め込まれている。なので2回目の i への代入時に「i が持っていたレシーバの情報 c[0]
が c[1]
上書きされてしてしまう事になり、結果 a, b ではなく b, b が表示される。Method values という仕組みは参考として見ておくと良い。i への代入時点でレシーバの情報が デリファレンス されコピーされた状態で代入されているのがポイント、f()
の呼び出し時の話ではない。
package main
import (
"fmt"
)
type a struct {
N string
}
func (s *a) f() func() {
return func() {
fmt.Printf("%s\n", s.N)
}
}
func main() {
b := []func(){}
c := []a{{"a"}, {"b"}}
var i a
i = c[0]
b = append(b, i.f())
b[0]()
i = c[1]
b[0]() // この時点で既に b になる
b = append(b, i.f())
}
まとめ
混ぜるな危険