Goのfor文でrangeを使用してイテレートする際にはイテレータを使いまわすため、意図した通りの挙動にならないことはあまりにも有名です。
A “for” statement with a ForClause is also controlled by its condition, but additionally it may specify an init and a post statement, such as an assignment, an increment or decrement statement. The init statement may be a short variable declaration, but the post statement must not. Variables declared by the init statement are re-used in each iteration.
脳死でループカウンタを使ってしまっていたので、自戒をこめて解説してみました。
type Dog struct {
name string
}func main() {
dogs := []Dog{
{name: "Bulldog"},
{name: "Italian Greyhound"},
{name: "Maltese"},
}
ptrDogs := make([]*Dog, 0, len(dogs))
for _, dog := range dogs {
ptrDogs = append(ptrDogs, &dog)
} for _, v := range ptrDogs {
fmt.Println(v)
}
}
実行結果は下記のようになります。
&{Maltese}
&{Maltese}
&{Maltese}
なんでだっけ。。。
修正前では同じポインタを代入し、値が書き変わってしまっているからです。
for _, dog := range dogs {
fmt.Printf("%d: %p\n", dog, &dog)
ptrDogs = append(ptrDogs, &dog)
}
実行すると、
Bulldog: 0xc000018070
Italian Greyhound: 0xc000018070
Maltese: 0xc000018070
うーんわかるようなわからないような。。。
dogs := []Dog{
{name: "Bulldog"},
{name: "Italian Greyhound"},
{name: "Maltese"},
}
まずこの部分のメモリ領域をみていきましょう。
stringのフィールドを一つ持つ構造体Dogの型サイズは16byteですが、dogsはスライスで参照型なので一律24byteです。
そのため、スライスのdogsのメモリ領域が確保されます。
次に説明をわかりやすくするためにfor文を分解します。
var dog Dog // イテレータを使いまわすのでこれが大事dog = dogs[0]
ptrDogs = append(ptrDogs, &dog)dog = dog[1]
ptrDogs = append(ptrDogs, &dog)dog = dog[2]
ptrDogs = append(ptrDogs, &dog)
Dog型16byte*3つ分のメモリが確保され、各要素ごとに隙間はないものの区切られていて、それぞれに先頭アドレスが存在します。
変数dogのメモリ領域の16byte分が確保されています。
dog = dogs[0]
dogにdogs[0]の値を代入したので両者の値は同じになりますが、先頭アドレスは異なります。
ptrDogs = append(ptrDogs, &dog)
ここでdogの領域の先頭アドレスをptrDogsに追加します。
くどいですが、ここで代入されるのはdogs[0]の先頭アドレスではありません。
これが何度実行されようとも、ptrDogsに追加されるのはdogの領域の先頭アドレスです。
for _, v := range ptrDogs {
fmt.Println(v)
}
この状態で上が実行されると、更新されたdogの値、つまりdogs[2]の値が出力されることになるので全て&{Maltese}が出力されます。
もちろん、もしイテレータに再割り当てが行われない場合は、以下のようにfor文を分解することができ、意図した通りに動きます。
if true {
dog := dogs[0]
ptrDogs = append(ptrDogs, &dog)
}if true {
dog := dog[1]
ptrDogs = append(ptrDogs, &dog)
}if true {
dog := dog[2]
ptrDogs = append(ptrDogs, &dog)
}
以上を理解した上で、以下のように修正します。
for _, dog := range dogs {
dog := dog
ptrDogs = append(ptrDogs, &dog)
}
または
for i := range dogs {
ptrDogs = append(ptrDogs, &dogs[i])
}
dog := dog
は変数シャドウイングって言うらしい
In computer programming, variable shadowing occurs when a variable declared within a certain scope (decision block, method, or inner class) has the same name as a variable declared in an outer scope.