プログラミング
記事内に商品プロモーションを含む場合があります

Go言語のfor~rangeのスライス更新でハマりがちなポイントと対策

Aru

Go言語のfor~rangeを使用してスライスを更新しようすると、意図しない挙動に遭遇することがあります。特に、ポインタ参照に関連して意図とは異なる動作をしがちです。この記事では、for~rangeを使用する場合に気を付けるポイントや注意事項を中心に解説したちと思います。

Go言語の基本的な構文については以下の記事まとめていますので参考にしてください。

あわせて読みたい
Go言語プログラミング入門|基本文法・構文総まとめ
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
あわせて読みたい
Go言語プログラミング入門|基本文法・構文総まとめ
Go言語プログラミング入門|基本文法・構文総まとめ

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とループ中のeaの要素をコピーしたもので、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++言語と比較して、値渡しと参照渡しを意識しない言語だからこそ、注意が必要だと感じました。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました