Goのループカウンタを理解する

ぺりー
6 min readNov 27, 2021

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.

Wikipedia

参考

--

--

ぺりー

Satoru Kitaguchi. Backend Engineer at eureka, Inc.