Goの繰り返しは for の1種類だけで、条件の書き方と range の対象を変えることで、他の言語の while・foreach・イテレータまでをすべて表現します。 書式は「3項」「条件のみ」「無限ループ」「range」の4系統で、range はスライス・map・文字列・チャネルに加えて、Go 1.22 から整数、Go 1.23 から関数も回せるようになりました。

この記事では、for の全書式を早見表にまとめたうえで、すべてのコードを go1.24.7 で実行し、出力をそのまま載せています。 「range で要素を書き換えたのに反映されない」理由と直し方、Go 1.22 でループ変数が反復ごとに新しくなった変更(go.mod の go 行を 1.21 と 1.24 に切り替えた出力の差)、ラベル付き break / continue、map の順序が毎回変わる点まで、つまずきやすい箇所を順に確かめます。

for文とrangeの早見表

やりたいこと 書き方 使える版
回数を決めて回す(3項) for i := 0; i < n; i++ { } すべて
条件が真の間だけ回す for n < 20 { } すべて
無限ループ for { } と break すべて
n 回だけ回す for i := range n { } Go 1.22 以降
値を使わず n 回 for range n { } Go 1.22 以降
スライス・配列を回す for i, v := range s { } すべて
値だけ使う for _, v := range s { } すべて
インデックスだけ使う for i := range s { } すべて
map を回す for k, v := range m { } すべて(順序は不定)
文字列を1文字ずつ for i, r := range str { } すべて(rune 単位)
チャネルから受け取る for v := range ch { } すべて(close まで)
関数(イテレータ)を回す for v := range seq { } Go 1.23 以降
今の周を飛ばす continue すべて
ループを抜ける break すべて
外側のループを抜ける break ラベル名 すべて
外側の次の周へ進む continue ラベル名 すべて

range が返す値は、対象によって意味が変わります。

range の対象 1つ目 2つ目 覚えておく点
整数 n 0 から n-1 なし n が 0 以下なら 0 回
スライス・配列 インデックス 要素のコピー 値を書き換えても元は変わらない
map キー 値のコピー 順序は実行ごとに変わる
文字列 バイト位置 rune(文字) 日本語は位置が 3 ずつ進む
チャネル 受け取った値 なし close されるまで待ち続ける
関数 yield に渡した値 2つ目の値(ある場合) break すると yield が false を返す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Goのfor文とrangeの使い方|break・continue・値コピーまで」

検証環境とコードの動かし方

この記事のコードは、次の環境で実行しました。

項目 内容
Go go1.24.7 linux/amd64
実行方法 go run main.go(ループ変数の比較だけ go.mod つきで go run .)
外部モジュール なし(標準ライブラリのみ)

どのコードも main.go という名前で保存し、go run main.go でそのまま動きます。出力の右側にある「<- 〇〇」は、どの書式の結果かがわかるようにコード側で付けた目印です。

for の全書式を1本のコードで確かめる

まず、9 種類の書式を 1 つのファイルにまとめて動かします。Go には while も foreach もなく、すべて for で書く という点を、出力と見比べながら確認してください。

package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {
    // 1. 3項(初期化; 条件; 後処理)
    for i := 0; i < 3; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println("<- 3項")

    // 2. 条件のみ(他言語の while)
    n := 1
    for n < 20 {
        n *= 2
    }
    fmt.Println(n, "<- 条件のみ")

    // 3. 無限ループ + break
    count := 0
    for {
        count++
        if count == 3 {
            break
        }
    }
    fmt.Println(count, "<- 無限ループ")

    // 4. range over 整数(Go 1.22 以降)
    for i := range 3 {
        fmt.Print(i, " ")
    }
    fmt.Println("<- range 3")

    // 5. スライス(インデックスと値)
    items := []string{"pen", "note", "tape"}
    for i, v := range items {
        fmt.Print(i, ":", v, " ")
    }
    fmt.Println("<- スライス")

    // 6. map(順序は毎回変わるので、キーを並べ替えてから回す)
    stock := map[string]int{"pen": 12, "note": 5, "tape": 0}
    for _, k := range slices.Sorted(maps.Keys(stock)) {
        fmt.Print(k, "=", stock[k], " ")
    }
    fmt.Println("<- map(キー順)")

    // 7. 文字列(バイト位置と rune)
    for i, r := range "Go言語" {
        fmt.Printf("%d:%c ", i, r)
    }
    fmt.Println("<- 文字列")

    // 8. チャネル(close されるまで受け取る)
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    ch <- 30
    close(ch)
    for v := range ch {
        fmt.Print(v, " ")
    }
    fmt.Println("<- チャネル")

    // 9. 関数(Go 1.23 以降のイテレータ)
    evens := func(yield func(int) bool) {
        for i := 0; i < 10; i += 2 {
            if !yield(i) {
                return
            }
        }
    }
    for v := range evens {
        fmt.Print(v, " ")
    }
    fmt.Println("<- 関数")
}

実行結果:

0 1 2 <- 3項
32 <- 条件のみ
3 <- 無限ループ
0 1 2 <- range 3
0:pen 1:note 2:tape <- スライス
note=5 pen=12 tape=0 <- map(キー順)
0:G 1:o 2:言 5:語 <- 文字列
10 20 30 <- チャネル
0 2 4 6 8 <- 関数

読み取れる点を整理します。

  • 3項の for は「初期化; 条件; 後処理」の順で、かっこは付けません。波かっこは省略できません。
  • 条件のみの for が、他の言語の while にあたります。
  • 無限ループ は for { } と書き、break か return で抜けます。
  • range 3 は 0・1・2 の 3 回です。回数だけ欲しい場面で 3項より短く書けます。
  • 文字列の range は rune(Unicode の1文字)単位で進み、1つ目の値は「何文字目」ではなく「何バイト目」です。「言」が 2、「語」が 5 になっているのはそのためです。
  • map は順序が決まっていないので、ここではキーを並べ替えてから回しています(後の章で詳しく扱います)。

3項・条件のみ・無限ループ・range の選び方

状況 向いている書式 理由
0 から n-1 まで順に回す for i := range n 短く、境界の書きまちがいが起きにくい
逆順・2つ飛ばし・2変数で回す 3項の for 増やし方を自由に書ける
スライスの全要素を読む for _, v := range s インデックスの管理が不要
スライスの要素を書き換える for i := range s s[i] で元の要素を直接さわれる
終わる条件が1つの式で書ける 条件のみの for 意図がそのまま読める
抜ける場所がループの途中にある for { } と break 条件を複数の場所に置ける
受信が終わるまで処理する for v := range ch close を自動で検知できる

よく使う書き方のコピペ集(逆順・2つずつ・文字列・nil)

日常的によく書くパターンを、1 つのファイルにまとめました。

package main

import "fmt"

func main() {
    s := "Go言語"
    fmt.Println("len(s) =", len(s))
    for i := 0; i < len(s); i++ {
        fmt.Printf("%x ", s[i])
    }
    fmt.Println("<- バイト単位")
    for i, r := range s {
        fmt.Printf("%d:%c ", i, r)
    }
    fmt.Println("<- range(rune 単位)")
    for i, r := range []rune(s) {
        fmt.Printf("%d:%c ", i, r)
    }
    fmt.Println("<- []rune に変換")
    for i, r := range "a\xffb" {
        fmt.Printf("%d:%U ", i, r)
    }
    fmt.Println("<- 不正なバイト")

    // 空・nil でも安全
    var ns []int
    var nm map[string]int
    for range ns {
        fmt.Println("実行されない")
    }
    for range nm {
        fmt.Println("実行されない")
    }
    fmt.Println("nil のスライスと map は0回")

    // 値を使わない書き方
    for range 3 {
        fmt.Print("* ")
    }
    fmt.Println("<- for range 3")

    // 逆順・2つずつ・2変数
    for i := 4; i >= 0; i -= 2 {
        fmt.Print(i, " ")
    }
    fmt.Println("<- 逆順で2つずつ")
    for i, j := 0, 4; i < j; i, j = i+1, j-1 {
        fmt.Print(i, "-", j, " ")
    }
    fmt.Println("<- 2変数")
}

実行結果:

len(s) = 8
47 6f e8 a8 80 e8 aa 9e <- バイト単位
0:G 1:o 2:言 5:語 <- range(rune 単位)
0:G 1:o 2:言 3:語 <- []rune に変換
0:U+0061 1:U+FFFD 2:U+0062 <- 不正なバイト
nil のスライスと map は0回
* * * <- for range 3
4 2 0 <- 逆順で2つずつ
0-4 1-3 <- 2変数
  • 文字列をバイト単位で扱いたいときは 3項の for、文字単位で扱いたいときは range を使います。「何文字目か」が欲しい場合は []rune(s) に変換してから回すと、インデックスが 0・1・2・3 と連番になります。
  • UTF-8 として読めないバイトが混ざっていると、range は U+FFFD(置換文字)を返して 1 バイトだけ進みます。止まらずに最後まで回ります。
  • nil のスライスや nil の map を range しても 0 回で終わり、パニックにはなりません。 事前の nil チェックは不要です。
  • 値もインデックスも使わないときは for range 3 のように変数を省略できます。

range の値はコピー:要素を書き換えたのに反映されないとき

range で受け取る 2 つ目の値は、要素そのものではなく要素のコピー です。そのため、ループの中で値を書き換えても、元のスライスや map には反映されません。再現コードと直し方を並べます。

package main

import "fmt"

type Item struct {
    Name  string
    Price int
}

func main() {
    items := []Item{{"pen", 100}, {"note", 200}, {"tape", 300}}

    // うまくいかない例:v はコピーなので、元のスライスは変わらない
    for _, v := range items {
        v.Price *= 2
    }
    fmt.Println("値で書き換え      :", items)

    // 直し方1:インデックス経由で書き換える
    for i := range items {
        items[i].Price *= 2
    }
    fmt.Println("インデックス経由  :", items)

    // 直し方2:要素へのポインタを取ってから書き換える
    for i := range items {
        p := &items[i]
        p.Price += 1
    }
    fmt.Println("ポインタ経由      :", items)

    // 直し方3:ポインタのスライスにしておく
    ptrs := []*Item{{"pen", 100}, {"note", 200}}
    for _, p := range ptrs {
        p.Price *= 2
    }
    fmt.Println("ポインタのスライス:", *ptrs[0], *ptrs[1])

    // int のスライスでも同じ
    nums := []int{1, 2, 3}
    for _, n := range nums {
        n *= 10
    }
    fmt.Println("int(値)         :", nums)
    for i := range nums {
        nums[i] *= 10
    }
    fmt.Println("int(インデックス):", nums)

    // range の対象は最初に1回だけ評価される
    s := []int{1, 2, 3}
    for i, v := range s {
        if i == 0 {
            s = append(s, 99) // ループ回数は増えない
        }
        fmt.Print(v, " ")
    }
    fmt.Println("<- 3回で終わる / len(s) =", len(s))

    // 配列を range すると配列全体のコピーが対象になる
    arr := [3]int{1, 2, 3}
    for i, v := range arr {
        if i == 0 {
            arr[2] = 300
        }
        fmt.Print(v, " ")
    }
    fmt.Println("<- 配列(コピーを回す)")
    sl := []int{1, 2, 3}
    for i, v := range sl {
        if i == 0 {
            sl[2] = 300
        }
        fmt.Print(v, " ")
    }
    fmt.Println("<- スライス(元の配列を参照)")

    // map の値も同じ:構造体の値はコピー
    m := map[string]Item{"a": {"pen", 100}}
    for k, v := range m {
        v.Price = 999
        _ = k
    }
    fmt.Println("map(値)         :", m)
    for k, v := range m {
        v.Price = 999
        m[k] = v // 入れ直す
    }
    fmt.Println("map(入れ直し)   :", m)
}

実行結果:

値で書き換え      : [{pen 100} {note 200} {tape 300}]
インデックス経由  : [{pen 200} {note 400} {tape 600}]
ポインタ経由      : [{pen 201} {note 401} {tape 601}]
ポインタのスライス: {pen 200} {note 400}
int(値)         : [1 2 3]
int(インデックス): [10 20 30]
1 2 3 <- 3回で終わる / len(s) = 4
1 2 3 <- 配列(コピーを回す)
1 2 300 <- スライス(元の配列を参照)
map(値)         : map[a:{pen 100}]
map(入れ直し)   : map[a:{pen 999}]

1 行目の「値で書き換え」が、もとの 100・200・300 のままになっている点に注目してください。コンパイルエラーにも警告にもならないため、気づきにくい箇所です。

症状と直し方の対応表

症状 理由 直し方
構造体のフィールドを変えたのに元が変わらない v は要素のコピー for i := range s にして s[i].Field を変える
int や string の要素を変えたのに元が変わらない 同じく v はコピー s[i] = 新しい値 と代入する
何度もフィールドを書き換えたい 毎回 s[i]. と書くのが長い p := &s[i] でポインタを取って p.Field を変える
要素が大きく、コピーを減らしたい 反復ごとに構造体全体がコピーされる インデックスで回す、またはポインタのスライス([]*Item)にする
map の構造体の値を変えたい m[k].Field = 1 は書けない 取り出して変更し、m[k] = v で入れ直す
ループ中に append したのに回数が増えない range の対象は最初に 1 回だけ評価される 追加分も回したいなら 3項の for で len(s) を毎回見る

配列とスライスで挙動が違う理由

出力の「配列(コピーを回す)」と「スライス(元の配列を参照)」を見比べてください。同じようにループの 1 周目で 3 番目の要素を 300 に書き換えていますが、配列では 3 のまま、スライスでは 300 になっています。

  • 配列を for i, v := range arr で回すと、range は最初に 配列全体をコピー し、そのコピーから値を取り出します。
  • スライスは「元の配列への参照・長さ・容量」の 3 つ組なので、コピーされるのはこの 3 つ組だけで、要素は同じ配列を見ています。

大きな配列を値つきで回すとコピーが発生するので、for i := range arr とインデックスだけにするか、range arr[:] や range &arr と書いてコピーを避けます。

Go 1.22 でループ変数が反復ごとに新しくなった

Go 1.22 のリリースノートには、for ループについて 2 つの変更が書かれています。1 つは整数を range できるようになったこと、もう 1 つが ループ変数が反復ごとに新しく作られるようになったこと です。以前は、ループで宣言した変数は 1 回だけ作られ、各反復で上書きされていました。

この違いは、クロージャ(外側の変数をつかんだ関数)や、変数のアドレスを保存するコードで出力の差として現れます。同じ main.go を、go.mod の go 行だけ変えて実行しました。

package main

import (
    "fmt"
    "sync"
)

func main() {
    // 1. クロージャがループ変数をつかむ
    var funcs []func()
    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() { fmt.Print(i, " ") })
    }
    for _, f := range funcs {
        f()
    }
    fmt.Println("<- クロージャ")

    // 2. ループ変数のアドレスを集める
    var ptrs []*int
    for _, v := range []int{10, 20, 30} {
        ptrs = append(ptrs, &v)
    }
    for _, p := range ptrs {
        fmt.Print(*p, " ")
    }
    fmt.Println("<- アドレス")

    // 3. goroutine に渡す
    var wg sync.WaitGroup
    results := make([]string, 3)
    for i, name := range []string{"a", "b", "c"} {
        wg.Add(1)
        go func() {
            defer wg.Done()
            results[i] = name
        }()
    }
    wg.Wait()
    fmt.Println(results, "<- goroutine")
}

go.mod(新しい仕様):

module loopvar

go 1.24

実行結果(go 1.24):

0 1 2 <- クロージャ
10 20 30 <- アドレス
[a b c] <- goroutine

go.mod の go 行を go 1.21 に書き換えて、同じコードを同じ go1.24.7 で実行した結果:

3 3 3 <- クロージャ
30 30 30 <- アドレス
[  c] <- goroutine
確認した内容 go 行が 1.21 go 行が 1.22 以降
クロージャが見る i 全部 3(ループ終了後の値) 0 1 2(その周の値)
保存したアドレスの中身 全部 30(同じ変数を指す) 10 20 30(別々の変数)
goroutine が見る i と name 最後の値に寄る(結果は実行ごとに変わりうる) その周の値

どちらの仕様で動くかは、インストールしている Go の版ではなく、go.mod の go 行で決まります。 新しいツールチェーンでも、go 行が 1.21 以前のモジュールは以前の仕様のまま動くので、既存のコードの挙動が勝手に変わることはありません。今回は Go 1.21 のツールチェーンそのものは使っておらず(未実行)、go 行の切り替えで 1.21 までの仕様を再現しています。

go 行が 1.21 のモジュールでは、go vet が次のように教えてくれます。

# loopvar
# [loopvar]
./main.go:36:12: loop variable i captured by func literal
./main.go:36:17: loop variable name captured by func literal

go 行が 1.21 以前のコードでの書き方

go 行を上げられない場合は、ループの先頭で同じ名前の変数を作り直すと、1.22 以降と同じ結果になります。

for i, name := range names {
    i, name := i, name // その周専用のコピーを作る
    go func() {
        results[i] = name
    }()
}

go 行が 1.22 以降なら、この 1 行は不要です。残っていても動作は変わりません。

3項の for と range over 整数の細かな違い

ループ変数を本体の中で書き換えたときの動きも確かめました。

package main

import "fmt"

func main() {
    // 3項 for でも反復ごとに新しい i。本体で i を変えると次の周に引き継がれる
    for i := 0; i < 6; i++ {
        if i == 1 {
            i += 2 // 2 と 3 を飛ばす
        }
        fmt.Print(i, " ")
    }
    fmt.Println("<- 本体で i を進める")

    // range over 整数では、本体で i を変えても次の周に影響しない
    for i := range 6 {
        if i == 1 {
            i += 2
        }
        fmt.Print(i, " ")
    }
    fmt.Println("<- range 6 で i を変える")

    // range の対象が 0 以下なら 0 回
    for i := range 0 {
        fmt.Println(i)
    }
    n := -3
    for i := range n {
        fmt.Println(i)
    }
    fmt.Println("range 0 と range -3 は 0 回")

    // 型付きの整数
    var total int64 = 3
    for i := range total {
        fmt.Printf("%T ", i)
    }
    fmt.Println("<- range int64")

    // defer はループを抜けたあと、関数の終わりにまとめて動く
    for i := range 3 {
        defer fmt.Print("defer", i, " ")
    }
    fmt.Println("ループ終了")
}

実行結果:

0 3 4 5 <- 本体で i を進める
0 3 2 3 4 5 <- range 6 で i を変える
range 0 と range -3 は 0 回
int64 int64 int64 <- range int64
ループ終了
defer2 defer1 defer0
  • 3項の for では、本体で i を進めると次の周に引き継がれます(i が 1 の周で 3 に進めると、次の周は 4 から続きます)。
  • range over 整数 では、本体で i を変えても次の周は予定どおりの値から始まります(3 を表示したあと、次の周は 2)。回数が確定しているので、読み手が安心できる書き方です。
  • range 0 や負の値は 0 回で、エラーにはなりません。
  • range に渡した整数の型が、そのままループ変数の型になります(int64 を渡せば int64)。
  • ループの中の defer は、その周の終わりではなく関数の終わりに、後に登録した順で動きます。 ファイルを開いて閉じる処理をループで書くときは、1 周分を別の関数に切り出すと、周ごとに閉じられます。

break と continue、ラベル付きで外側のループを抜ける

break はいちばん内側のループを抜け、continue は今の周の残りを飛ばして次の周へ進みます。二重ループで外側ごと抜けたいときは、for の直前にラベルを付けて break ラベル名 と書きます。

package main

import "fmt"

func main() {
    grid := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}

    // ラベルなし break:内側のループだけ抜ける
    for _, row := range grid {
        for _, v := range row {
            if v == 5 {
                break
            }
            fmt.Print(v, " ")
        }
    }
    fmt.Println("<- ラベルなし break")

    // ラベル付き break:外側のループごと抜ける
outer:
    for _, row := range grid {
        for _, v := range row {
            if v == 5 {
                break outer
            }
            fmt.Print(v, " ")
        }
    }
    fmt.Println("<- break outer")

    // ラベル付き continue:外側の次の周へ進む
rows:
    for _, row := range grid {
        for _, v := range row {
            if v%2 == 0 {
                continue rows
            }
            fmt.Print(v, " ")
        }
    }
    fmt.Println("<- continue rows")

    // continue:偶数だけ飛ばす
    for i := range 6 {
        if i%2 == 0 {
            continue
        }
        fmt.Print(i, " ")
    }
    fmt.Println("<- continue")

    // switch の中の break は switch を抜けるだけ
    for i := range 5 {
        switch i {
        case 2:
            break // for は止まらない
        }
        fmt.Print(i, " ")
    }
    fmt.Println("<- switch 内の break")

loop:
    for i := range 5 {
        switch i {
        case 2:
            break loop // for ごと抜ける
        }
        fmt.Print(i, " ")
    }
    fmt.Println("<- break loop")

    // select の中でも同じ
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    close(ch)
recv:
    for {
        select {
        case v, ok := <-ch:
            if !ok {
                break recv
            }
            fmt.Print(v, " ")
        }
    }
    fmt.Println("<- select + break recv")
}

実行結果:

1 2 3 4 7 8 9 <- ラベルなし break
1 2 3 4 <- break outer
1 7 <- continue rows
1 3 5 <- continue
0 1 2 3 4 <- switch 内の break
0 1 <- break loop
1 2 <- select + break recv
書き方 動き 出力で確かめた点
break いちばん内側の for を抜ける 5 と 6 だけ飛び、7 8 9 は表示される
break outer ラベルの付いた for ごと抜ける 1 2 3 4 で終わる
continue rows ラベルの付いた for の次の周へ進む 各行で偶数に当たった時点で次の行へ
continue 今の周の残りを飛ばす 奇数だけ表示される
switch の中の break switch だけを抜ける for は止まらず 0〜4 すべて表示
switch の中の break loop for ごと抜ける 0 1 で終わる

switch や select の中に書いた break は、for ではなく switch / select を抜けます。 Go の switch は case の終わりで自動的に抜けるので、case の中の break は実質なにもしません。ループを止めたいときはラベルを付けるか、return を使います。チャネルの受信を select で待つループでは、この書き方が定番です。

ラベルは「for の直前の行」に置き、慣習として行頭に寄せて書きます。gofmt をかけると自動でこの形に整います。

map の順序は毎回変わる:並べ替えて回す

map を range したときの順序は、仕様として決まっていません。同じ map を 3 回まわすだけでも、開始位置が変わることがあります。

package main

import (
    "fmt"
    "maps"
    "slices"
)

func main() {
    stock := map[string]int{"pen": 12, "note": 5, "tape": 0, "clip": 30, "glue": 7}

    // 同じ map を3回まわす
    for n := range 3 {
        fmt.Print(n+1, "回目: ")
        for k := range stock {
            fmt.Print(k, " ")
        }
        fmt.Println()
    }

    // キー順に並べる
    fmt.Print("キー順 : ")
    for _, k := range slices.Sorted(maps.Keys(stock)) {
        fmt.Print(k, "=", stock[k], " ")
    }
    fmt.Println()

    // 値の大きい順に並べる
    keys := slices.Collect(maps.Keys(stock))
    slices.SortFunc(keys, func(a, b string) int {
        if stock[a] != stock[b] {
            return stock[b] - stock[a]
        }
        return 0
    })
    fmt.Print("値の降順: ")
    for _, k := range keys {
        fmt.Print(k, "=", stock[k], " ")
    }
    fmt.Println()

    // ループ中の削除は安全
    for k, v := range stock {
        if v == 0 {
            delete(stock, k)
        }
    }
    fmt.Println("削除後の件数:", len(stock))

    // fmt.Println はキー順に表示する
    fmt.Println(stock)
}

実行結果(1〜3回目の並びは実行するたびに変わります):

1回目: pen note tape clip glue
2回目: note tape clip glue pen
3回目: pen note tape clip glue
キー順 : clip=30 glue=7 note=5 pen=12 tape=0
値の降順: clip=30 pen=12 glue=7 note=5 tape=0
削除後の件数: 4
map[clip:30 glue:7 note:5 pen:12]
  • 順序が必要なら、キーを取り出して並べ替えてから回します。 Go 1.23 以降は slices.Sorted(maps.Keys(m)) の 1 行で書けます。
  • 値の大きい順などにしたいときは、slices.Collect(maps.Keys(m)) でキーのスライスを作り、slices.SortFunc で並べ替えます。
  • range の途中で delete しても安全です。 まだ回っていない要素を消した場合、その要素は出てこなくなるだけです。
  • fmt.Println(m) はキー順に整列して表示します。デバッグ表示が毎回同じ順なのは fmt 側の配慮で、range の順序とは別の話です。

テストで「1回目は通ったのに次は通らない」という現象が出たら、map の順序に結果が左右されていないかを確かめてみてください。

range over 関数(イテレータ)の書き方

Go 1.23 から、決まった形の関数を range に渡せる ようになりました。Go 1.23 のリリースノートでは、次の 3 つの型が range の対象として挙げられています。

関数の型 for での受け取り方 iter パッケージの別名
func(func() bool) for range f なし
func(func(K) bool) for v := range f iter.Seq[V]
func(func(K, V) bool) for k, v := range f iter.Seq2[K, V]

引数の関数(慣習で yield と呼びます)に値を渡すたびに、for の本体が 1 回動きます。本体で break すると yield が false を返すので、イテレータ側はそこで return します。

package main

import (
    "fmt"
    "iter"
    "maps"
    "slices"
)

// Countdown は n から 1 までを順に返すイテレータ
func Countdown(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := n; i >= 1; i-- {
            if !yield(i) {
                fmt.Print("(途中で終了) ")
                return
            }
        }
    }
}

// Enumerate は 1 始まりの番号と値を返すイテレータ
func Enumerate[T any](s []T) iter.Seq2[int, T] {
    return func(yield func(int, T) bool) {
        for i, v := range s {
            if !yield(i+1, v) {
                return
            }
        }
    }
}

func main() {
    for v := range Countdown(3) {
        fmt.Print(v, " ")
    }
    fmt.Println("<- Countdown(3)")

    for v := range Countdown(5) {
        if v == 3 {
            break
        }
        fmt.Print(v, " ")
    }
    fmt.Println("<- break で止める")

    for no, name := range Enumerate([]string{"pen", "note"}) {
        fmt.Print(no, ".", name, " ")
    }
    fmt.Println("<- Enumerate")

    s := []string{"a", "b", "c"}
    for i, v := range slices.Backward(s) {
        fmt.Print(i, ":", v, " ")
    }
    fmt.Println("<- slices.Backward")

    for v := range slices.Values(s) {
        fmt.Print(v, " ")
    }
    fmt.Println("<- slices.Values")

    m := map[string]int{"b": 2, "a": 1}
    fmt.Println(slices.Sorted(maps.Keys(m)), "<- slices.Sorted(maps.Keys(m))")
    fmt.Println(slices.Collect(Countdown(4)), "<- slices.Collect")

    for c := range slices.Chunk([]int{1, 2, 3, 4, 5}, 2) {
        fmt.Print(c, " ")
    }
    fmt.Println("<- slices.Chunk")
}

実行結果:

3 2 1 <- Countdown(3)
5 4 (途中で終了) <- break で止める
1.pen 2.note <- Enumerate
2:c 1:b 0:a <- slices.Backward
a b c <- slices.Values
[a b] <- slices.Sorted(maps.Keys(m))
[4 3 2 1] <- slices.Collect
[1 2] [3 4] [5] <- slices.Chunk
  • 「(途中で終了)」と表示されているとおり、for 側の break はイテレータに false として伝わります。 if !yield(v) { return } の形を必ず書いておきます。
  • 標準ライブラリにも、slices.Backward(逆順)、slices.Values、slices.Chunk(n 個ずつ)、maps.Keys、maps.Values など、イテレータを返す関数がそろっています。
  • イテレータをスライスに戻すときは slices.Collect、並べ替えたスライスにするときは slices.Sorted を使います。

自作のイテレータは、ページ送りしながら取得する処理や、木構造をたどる処理のように「次の値の取り方が複雑なもの」を、呼び出し側からは普通の for に見せたいときに役立ちます。単純なスライスであれば、range をそのまま使うほうが読みやすくなります。

つまずきやすいコンパイルエラーと直し方

for と range まわりで出やすいエラーを、実際にビルドして集めました(go1.24.7 のメッセージ)。

書いたコード エラーメッセージ 直し方
go 行が 1.21 で for i := range 3 cannot range over 3 (untyped int constant): requires go1.22 or later (-lang was set to go1.21; check go.mod) go.mod の go 行を 1.22 以上にする
go 行が 1.22 で関数を range cannot range over seq (variable of type func(yield func(int) bool)): requires go1.23 or later (-lang was set to go1.22; check go.mod) go 行を 1.23 以上にする
for i, v := range items で i を使わない declared and not used: i for _, v := range items にする
使っていないラベルがある label outer defined and not used ラベルを消すか、break / continue で使う
存在しないラベルへ break break label not defined: inner for の直前にラベルを置く
float64 を range cannot range over x (variable of type float64) int(x) に変換するか 3項の for にする

Go は 使っていない変数とラベルをコンパイルエラーにする ので、インデックスが不要なら _(ブランク識別子)で受けるのが基本です。

for と range のチェックリスト

確認すること 見るポイント
要素を書き換えているか 書き換えるなら s[i] 経由になっているか
map の順序に頼っていないか 表示やテストの期待値が順序に左右されないか
switch / select の中の break for を止めたいならラベルか return になっているか
go.mod の go 行 1.22 以上か(ループ変数・range over 整数)、1.23 以上か(range over 関数)
ループ内の defer 周ごとに後始末したい処理が関数の終わりまで残っていないか
チャネルの range 送る側が close しているか(close されないと待ち続ける)
文字列の range 1つ目の値をバイト位置として扱っているか

よくある質問

Q. Go に while 文はありますか? A. ありません。for 条件 { } が while にあたり、for { } が無限ループです。do-while にあたる書き方は、for { 処理; if !条件 { break } } と書きます。

Q. range で値を書き換えても反映されないのはなぜですか? A. 2 つ目の値は要素のコピーだからです。for i := range s に変えて s[i] を書き換えると反映されます。今回の検証でも、値で書き換えたスライスは 100・200・300 のままで、インデックス経由では 200・400・600 になりました。

Q. Go 1.22 以降なら、ループ内で i := i と書く必要はありませんか? A. go.mod の go 行が 1.22 以上であれば不要です。go 行が 1.21 以前のモジュールでは以前の仕様で動くため、go 行を確かめてから外してください。

Q. map を毎回同じ順で回すにはどうしますか? A. キーを並べ替えてから回します。Go 1.23 以降なら for _, k := range slices.Sorted(maps.Keys(m)) と書けます。

まとめ

  • Go の繰り返しは for だけ。 3項・条件のみ・無限ループ・range の 4 系統を使い分けます。
  • range の 2 つ目の値はコピー。 要素を書き換えるときは s[i] 経由にします。
  • Go 1.22 からループ変数は反復ごとに新しくなり、クロージャや goroutine がその周の値を見るようになりました。切り替わるかどうかは go.mod の go 行で決まります。
  • range over 整数は Go 1.22、range over 関数は Go 1.23 から 使えます。
  • 外側のループを抜けるにはラベル付き break。 switch / select の中の break は for を止めません。
  • map の順序は毎回変わる ので、順序が必要なら slices.Sorted(maps.Keys(m)) で並べ替えます。
  • 使わない変数は _ で受け、nil のスライスや map はそのまま range できます。

参考・出典

  • Go 1.22 Release Notes(Changes to the language): https://go.dev/doc/go1.22
  • Go 1.23 Release Notes(Changes to the language): https://go.dev/doc/go1.23

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Goのfor文とrangeの使い方|break・continue・値コピーまで」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?