Menu

Go言語のrange:すべての形、返す値、落とし穴

for rangeがスライス、配列、文字列、マップ、チャネル、整数、イテレータ関数に対して何を返すのか、値の変数がコピーである理由、rangeしながらスライスやマップを変更すると何が起きるのか、そしてGo 1.23の関数に対するrangeを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

range は、コレクションをたどる形の for です。反復ごとに1つまたは2つの値が得られ、その値が何になるかはrangeする型によって決まります。

オペランド1つ目の値2つ目の値備考
スライス []T、配列 [n]Tインデックス int要素 T、コピー
配列へのポインタ *[n]Tインデックス要素
文字列バイトインデックス intruneUTF-8をデコードする
マップ map[K]Vキー KV順序はランダム
チャネル chan T要素 Tなしチャネルが閉じられるまで
整数 n0からn-1なしGo 1.22
func(yield func() bool)なしなしGo 1.23
func(yield func(V) bool)VなしGo 1.23
func(yield func(K, V) bool)KVGo 1.23

受け取る変数を選ぶ

for i, v := range s {}  // both
for i := range s {}     // index (or key) only
for _, v := range s {}  // value only
for range s {}          // neither: just repeat len(s) times

使わない変数を宣言するとコンパイルエラーになるので、不要なものは省くか _ にします。for range 3 {} は、何かを3回繰り返す最も短い書き方です。

値はコピー

反復のたびに、要素が値の変数にコピーされます。変数を変更しても、コレクションは変わりません。

要素が大きな構造体だと、コピーには時間もかかります。大きな構造体のスライスでは、for i := range ss[i] を使えばコピーを避けられます。

文字列はルーンを返す

文字列に対するrangeはUTF-8をデコードします。1つ目の値は各文字が始まる バイト オフセットなので、数字が飛ぶことがあります。

文字ではなくバイトをたどるには、インデックスのループ for i := 0; i < len(s); i++ を使うか、[]byte(s) に対してrangeします。UTF-8の詳細はルーンとバイトのページを参照してください。

マップ:順序はランダム

マップの反復順は規定されておらず、コードが特定の順序に依存しないようにランタイムがランダムにしています。決まった順序で出力するには、キーをソートします。

rangeしながらエントリを削除するのは安全で、まだ到達していない削除済みのエントリは返されません。エントリの追加も許されていますが予測できません。新しいキーが同じループの後半で現れるかどうかはわかりません。マップを fmt.Println で表示するとキーがソートされるので、最後の行は安定しています。

チャネル:閉じられるまで

チャネルに対するrangeは、チャネルが閉じられて空になるまで値を受信します。誰も閉じなければループは永遠にブロックします(そしてすべてのゴルーチンがブロックすると、ランタイムは fatal error: all goroutines are asleep - deadlock! で停止します)。

チャネルを閉じるのは送信側で、受信側は決して閉じません。バッファ付きチャネルと select についてはチャネルのページを参照してください。

整数(Go 1.22)

range n は0から n の手前まで数えます。ループ変数の型は n の型になります。

for i := range 3 {}         // i is int: 0, 1, 2
for i := range uint8(3) {}  // i is uint8
for range 0 {}              // runs zero times

n が負の場合も0回実行されます。n はループが始まる前に一度だけ評価されます。

rangeが何をいつ評価するか

range の後ろの式は、最初の反復の前に 一度だけ 評価されます。スライスの場合、range はそのスライスヘッダーを使い続けるので、長さは固定されます。

ループの長さが3に固定されたので、追加した 99 は走査されません。一方 nums[2] = 30 は見えます。その時点では nums が、rangeしているスライスと元の配列を共有しているからです。2行を入れ替えると 30 も見えなくなります。append が容量3を超えて新しい配列を確保し、書き込みはそちらに入るからです。配列 の値に対するrangeは最初に配列全体をコピーするので、arr[2] への変更は見えません。コピーを避けるには &arrarr[:] に対してrangeします。

関数に対するrange(Go 1.23)

Go 1.23から、range はイテレータ関数も受け付けます。イテレータは yield コールバックを受け取り、値ごとに1回それを呼び、yieldfalse を返したら止まります(ループ本体が breakreturn を実行するとそうなります)。

iter.Seq[V]iter.Seq2[K, V] は、2つのイテレータの形の標準的な名前です。標準ライブラリでは slices.Allslices.Valuesslices.Backwardmaps.Keysmaps.Valuesmaps.All がこれらを返し、Go 1.24では strings.SplitSeqstrings.Linesbytes.SplitSeq も返します。イテレータは yield の戻り値を尊重しなければなりません。false が返った後に再び yield を呼ぶとpanicします。

ループ変数は反復ごと(Go 1.22)

Go 1.22以降、for ... range で宣言した変数は反復ごとに新しくなります。クロージャやゴルーチンで v を捕捉すると、最後の要素を保持することになる共有の変数ではなく、その反復の値を捕捉します。Go 1.22より前はループ内に v := v が必要でしたが、その行はもう不要です。このルールは go.modgo のバージョンで決まるので、古いモジュールは古い挙動のままです。

よくある間違い

  • 値の変数のアドレスを取る。 Go 1.22より前は、ptrs = append(ptrs, &v) は毎回同じアドレスを保存していました。1.22以降は別々のアドレスになりますが、どれもスライスの要素ではなくコピーを指しています。スライスの中を指すには &items[i] を使います。
  • マップの順序を期待する。 小さなテストでソートされているように見える出力も、ずっとソートされたままではありません。
  • 誰も閉じないチャネルに対してrangeする。 ループと、それを実行しているゴルーチンは永遠に待ち続けます。
  • 大きな配列を値でrangeする。 最初に配列全体がコピーされます。代わりにそのスライスに対してrangeします。

よくある質問

Goのrangeは何を返しますか?

オペランドによって決まります。スライスや配列ならインデックスと要素、文字列ならバイトインデックスとルーン、マップならキーと値、チャネルなら受信した各値、整数 n(Go 1.22)なら0からn-1、イテレータ関数(Go 1.23)なら関数が渡すものです。2つ目の変数は省略でき、1つ目は _ で捨てられます。

なぜrangeの値を変更してもスライスが変わらないのですか?

値の変数は要素のコピーだからです。for _, v := range items { v.Price = 0 } はコピーだけを変更します。代わりにインデックスを使うか:for i := range items { items[i].Price = 0 }、ポインタのスライスに対してrangeします。

Goで整数に対してrangeできますか?

Go 1.22からできます:for i := range 5 { ... }i を0から4まで動かして実行します。for range 5 { ... } は変数なしで5回繰り返します。モジュールの go.modgo 1.22 以降を指定している必要があります。

rangeしている最中にマップから削除しても安全ですか?

安全です。まだ到達していないエントリを削除すれば、それは返されなくなり、現在のエントリを削除しても問題ありません。ループ中にエントリを追加することも許されますが、新しいエントリが走査されるかどうかはわからないので、それに頼ってはいけません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める