Arantium Maestum

プログラミング、囲碁、読書の話題

Goのforループの変数スコープについてのメモ

以前Pythonのfor-loopでの変数スコープについてちょこっと書いた。

zehnpaard.hatenablog.com

最近Go言語でも似たような(?)問題があることを知ったので備忘録として残しておく。ちなみに私はGo言語は資料を少し読んだことはあるが書いたことはないので正確性は担保できない・・・

こちらのGoのGithub Issueに詳しい:

github.com

var all []*Item
for _, item := range items {
    all = append(all, &item)
}

などとやるとallに入っているポインタ(appendしているのが&itemでアドレスを取ってる)がすべて同じものになってしまう、という問題。

どういうことかというとfor _, item ...の部分で作成されるitemという変数は一回だけメモリ上で確保され、itemsの要素が一つずつその同じメモリ箇所を上書きすることで続くブロックの中でアクセスできるようになっている。なので各イテレーションでリストに追加するのが値ではなくアドレスである場合、同一のアドレスが追加されるようになってしまう。

調べてみるとStackOverflowでもすぐに出てきた:

stackoverflow.com

解決方法としては

var all []*Item
for i := range items {
    all = append(all, &items[i])
}

itemsに添字アクセスすることで、元リストのアドレスを新リストに入れるか、あるいは:

var all []*Item
for _, item := range items {
    item := item
    all = append(all, &item)
}

ブロック内でさらにitemという変数を導入することでブロックごとに新しくメモリを確保するか。こちらは少し非直感的なコードな気がする。この二つの例は挙動がけっこう違うのでどの挙動を求めているのかはっきり考える必要がある。

さて、元のGitHub Issueではこの挙動を変えるべきか、という議論が行われており、基本的には変える方向に好意的な意見が多い印象。C#でも似たような挙動を修正した経緯があったらしく、その開発者の一人がその修正の経験について語っていたりする。非直感的な挙動が修正されるのはいいことなんだとは思うが、プログラミング言語好きとしてはこういう少し謎な挙動があると気分が盛り上がるので修正されてしまっては少し寂しい気もしてしまう。