Goのrange内で使うポインタには気をつけよう(自戒)
はじめに
先日、Goのサーバー開発しているときに意図しない挙動があった。
調べてみるとrange
内で使用しているポインタに問題がありそうだった。今回は自戒も含めて問題とその解決方法を残しておく。
問題のコード
自分が書いていたコードとはかなり違うが、問題を分かりやすくするために以下のコードを用意した。userPtrs
というスライスに、range
内でusers
の要素のポインタを入れている。
package main
import "fmt"
type User struct {
Name string
}
func main() {
users := []User{
{"Alice"},
{"Bob"},
{"Charlie"},
}
var userPtrs []*User
for _, user := range users {
userPtrs = append(userPtrs, &user)
}
for _, u := range userPtrs {
fmt.Println(u.Name)
}
}
一見すると正しそうに見えるが、実行すると出力は以下になる。すべての要素がusers
の最後の要素を指してしまっている。
Charlie
Charlie
Charlie
原因と回避方法
この問題の原因は、range
で使用しているuser
変数がループごとに上書きされているためだ。よく考えれば当然なのだが、range
ではuser
という変数を定義してループごとにその変数に次の要素を再代入しているに過ぎない。つまり、user
変数が指すポインタはrange
を通して同じなのだ。
結果として、userPtrs
スライスの全ての要素が最終的に同じポインタ、つまり最後のuser
変数の状態を指すことになってしまったというわけだ。
これを回避するにはusers
の要素をインデックスを使って直接指定する変数を作成すれば良い。
for i := range users {
user := users[i]
userPtrs = append(userPtrs, &user)
}
これで期待する結果を得られた。
おわり
似たトピックとして、goroutineを扱う際のrange
は匿名関数を使う、というのがあるがrange
にまつわる事故は他にもありそう。
それを防ぐためにも気になる箇所はテストはしっかり書いておきたい。今回もテストに救われた。