Go言語のfor~rangeのスライス更新でハマりがちなポイントと対策
Go言語のfor~rangeを使用してスライスを更新しようすると、意図しない挙動に遭遇することがあります。特に、ポインタ参照に関連して意図とは異なる動作をしがちです。この記事では、for~range
を使用する場合に気を付けるポイントや注意事項を中心に解説したちと思います。
Go言語の基本的な構文については以下の記事まとめていますので参考にしてください。
Go言語のfor~range
for~rangeの基本的な使い方
Go言語でfor~range
を使う場合、基本的な記述は以下のようになります。
package main
import (
"fmt"
)
func main() {
a := []int{1, 2, 3, 4}
for _, e := range a {
fmt.Println(e)
}
}
for~range
の場合、1つめ(プログラム中では_
)が要素のインデックスで、2つめ(プログラム中ではe
)が要素の値になります。
上のプログラムでは、インデックスは受け取らずに、要素の値だけを受け取って要素の値を表示するものです。実行結果は以下のようになります。
実行結果
1
2
3
4
aの値を更新したい(NGな例)
for~range
のループの中で、a
の要素を更新したい場合、以下のように記述したくなると思います。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
for _, e := range a {
e += 10
}
fmt.Println(a) // NG: aの値は更新されない
}
実際に実行してみると、結果は以下のようになりa
の値は更新されません。
実行結果
[1 2 3 4]
これは、配列a
とループ中のe
はa
の要素をコピーしたもので、a
の要素を指しているわけではないからです。
アドレスを渡したい
例えば、C++の範囲ベースforの場合は、&e
としてaの値を書き換えることが可能なので、同じように記述できないかと考えるかもしれません。
例えば、下記のプログラムのように記述する方法です。
残念ながら、これはエラーになります。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
for _, &e := range a { // &eはエラー
fmt.Println(e)
}
}
Go言語では、for~range
で&を使うことはできません。
そもそも、アドレスはどうなっているのか?
ここで、e
のアドレスを確認してみたいと思います。確認するためには、以下のようなコードを実行します。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
for i, e := range a {
fmt.Println(e, &e, &a[i])
}
}
実行結果をみるとe
のアドレスとa[i]
のアドレスは異なっていることがわかります。また、e
のアドレスは常に同じです(これによるトラブルも発生します。それについては番外編で説明します)。
実行結果
1 0xc0000120e0 0xc0000220a0
2 0xc0000120e0 0xc0000220a8
3 0xc0000120e0 0xc0000220b0
4 0xc0000120e0 0xc0000220b8
ということで、for~range
ループでは、「e
は同じアドレスであり、a
の要素の値に毎回書き換えている」ことがわかりました。
元の配列の値を変更する場合の書き方
では、元の配列a
の中身を変更したい場合はどうすれば良いかというと、for~range
を使う場合は、以下のようにするのが良いかと思います。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
for i := range a {
a[i] += 10
}
fmt.Println(a)
}
for i:=0; i < len(a); i++ {...}
と書くのとあまり変わらない気もしますが、for~range
を使った方が少しだけ記述量が少ないです。
以上のように、for~range
で元のスライスの値を変更したい場合は注意が必要です。書き換えたつもりが書き換えられていないというバグの発生に注意しましょう。
C++やpythonでも同様の注意を行う必要があります。値渡しと参照渡しの問題は結構わかりにくいので注意が必要です。
番外編
今回、Go言語のfor~range
の挙動を調べていて、注意すべき挙動に気づいたので番外編として紹介しておきます。
ポインタに注意
以下のようなコードを考えます。
b
はポインタ配列で、e
の値をコピーせずにアドレスを配列要素にしています。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4}
b := []*int{}
for _, e := range a {
b = append(b, &e)
}
for i := 0; i < len(b); i++ {
fmt.Println(*b[i])
}
}
このプログラムを実行すると以下のような結果になります。
実行結果
4
4
4
4
e
が同じアドレスを使い回していることを思い出せば理解できると思いますが、bに格納されるアドレスは全て同じになり、全て最後の値になります。
整数配列などでこういうミスをすることはないかと思いますが、例えば構造体などの場合はコピーのコストを削減するためにアドレスだけ参照するということをやりがちなので注意が必要です。
まとめ
for~range
も便利ですが、注意して使わないと思わぬミスを起こしてしまうことに気づきました。C,C++言語と比較して、値渡しと参照渡しを意識しない言語だからこそ、注意が必要だと感じました。