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にまつわる事故は他にもありそう。
それを防ぐためにも気になる箇所はテストはしっかり書いておきたい。今回もテストに救われた。