- Go言語
Goのfor文とrangeの使い方|break・continue・値コピーまで
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期連続の黒字決算。