GolangのRange Clauseの落とし穴

Golangでrangeを使っていたら、変な落とし穴にハマって辛かったのでその話。

Range Clauseとは

いわゆるrange節とか範囲forとかって呼ばれるやつです。

var hoge [10]int
for i, v := range hoge {
}

と書くことで、変数vには常にhoge[i]が代入されてlen(hoge)回ループを回してくれます。

Range Clauseは配列とかスライスとかマップとかを線形に探索したいときとかによく書きますけど、ポインタが関係してくるとちょっとややこしい挙動をします。

 

ハマった経緯

伝統的なforを書いてるコードを可能であればRange Clauseに置き換えるリファクタリングツールを書いてたときに、概ね機械的に置き換えても正常に動いてくれたんですけど、ちょこちょこそのままだとバグるケースがあってハマりました。

 

ハマったケース

こういうケースをRange Clauseに置き換えようとするとハマります。

giste6964a99d9d42b65d46b24e75a4ab4f4

これを実行してみると、

$ go run range.go
0 0 9
1 1 9
2 2 9
3 3 9
4 4 9
5 5 9
6 6 9
7 7 9
8 8 9
9 9 9

えー、parr2のコピーがバグってる。

 

何が起こったのか

アドレスを表示させてみると大体何が起こったのかが分かります。

gista7bb1d128aab13258de363af8876d874

$ go run range2.go
0 0xc00008a000
1 0xc00008a000
2 0xc00008a000
3 0xc00008a000
4 0xc00008a000
5 0xc00008a000
6 0xc00008a000
7 0xc00008a000
8 0xc00008a000
9 0xc00008a000

あー、vはずっと同じアドレスなのね......

:=を使ってても毎回同じ変数が割り当てられるんですね。

 

まとめ

Range Clauseで作った変数は「:=」を使ってても実際は毎回同じ変数を使ってるのでアドレスには注意しようねっていう話でした。

今回挙げたサンプルコードの場合なら初見でもなんとなくヤバそうな感じが分かるんですけど、これが型エイリアスのついた構造体のポインタ配列とかになってくると知っていても普通に踏み抜きそう。実際「ポインタ配列にエイリアスをつける」っていう実装はかなり頻繁に見かけるので気をつけないとなぁ......

しかも今回は「伝統的なforからRange Clauseへの置き換えを自動化」をしようとしてハマったので、こういう踏み方をするとマジで手が出せなくなるんですよね。