ブログに戻る

Go の for キーワード:ループ、range、ループ変数、ラベル

Go の for キーワードの仕組みを解説します。3 つのループの形、slice、map、文字列、channel、整数、イテレーターに対する for range、Go 1.22 のループ変数、ラベル付きの break と continue、ループでよくある間違いを扱います。

Go の for キーワード:ループ、range、ループ変数、ラベル

Go でループを書くためのキーワードは for だけです。形は 3 つあります。3 つの節を持つループ for i := 0; i < n; i++、Go の while ループにあたる条件だけのループ for cond、そして無限ループ for { ... } です。range を付ければ、同じキーワードで slice、配列、map、文字列、channel、整数、イテレーター関数を反復できます。Go には while、do、foreach、loop といったキーワードはありません(Go spec, For statements)。

要約

  • for init; cond; post { ... } は回数を数えるループです。節を丸括弧で囲まず、波括弧は必須です。
  • for cond { ... } は while ループで、for { ... } は break、return、panic のいずれかが起きるまでループします。
  • for i, v := range x は slice、配列、map、文字列、channel、整数(Go 1.22)、イテレーター関数(Go 1.23)を反復します。map の順序はランダムです。
  • Go 1.22 以降、go.mod で go 1.22 以上を宣言していれば、反復ごとに新しいループ変数が作られます。ループの中で起動した goroutine が、そろって最後の値を見ることはもうありません。
  • range の値はコピーです。slice を変更するには items[i] に代入します。
  • select や switch の中でラベルなしの break を使うと、抜けるのはその文だけです。ループを抜けるにはラベルを使います。

Go の for ループにはどんな形があるのか?

仕様が定義している for 文は 1 つで、形が 3 通りあります。違いは、どの節を書くかだけです。

3 つの節を持つループ

初期化文は最初に 1 回だけ実行され、条件は各反復の前に評価され、後処理文は各反復の後に実行されます。次のループは、不安定なヘルスチェックを指数バックオフでリトライします。

example.gogo
package main

import (
	"errors"
	"fmt"
	"time"
)

var errUnavailable = errors.New("503 service unavailable")

func fetchStatus(attempt int) error {
	if attempt < 3 {
		return errUnavailable
	}
	return nil
}

func main() {
	backoff := 100 * time.Millisecond
	for attempt := 1; attempt <= 5; attempt++ {
		err := fetchStatus(attempt)
		if err == nil {
			fmt.Printf("attempt %d: ok\n", attempt)
			break
		}
		fmt.Printf("attempt %d: %v, retrying in %v\n", attempt, err, backoff)
		time.Sleep(backoff)
		backoff *= 2
	}
}
example.texttext
attempt 1: 503 service unavailable, retrying in 100ms
attempt 2: 503 service unavailable, retrying in 200ms
attempt 3: ok

attempt はループの中にしか存在しません。3 つの節はどれも省略できます。for ; attempt <= 5; { も正しいコードで、gofmt はこれを for attempt <= 5 { に書き換えます。

C のように節を丸括弧で囲む書き方は、コンパイルできません。

example.gogo
	for (i := 0; i < 3; i++) {
example.texttext
./main.go:6:9: syntax error: unexpected :=, expected )

条件だけのループ(while)

初期化文と後処理文を省くと、for は while ループになります。次のループは、ジョブキューが空になるまで処理を続けます。

example.gogo
package main

import "fmt"

func main() {
	queue := []string{"resize:42.png", "resize:43.png", "thumbnail:44.png"}

	for len(queue) > 0 {
		job := queue[0]
		queue = queue[1:]
		fmt.Println("processing", job)
	}
	fmt.Println("queue empty")
}
example.texttext
processing resize:42.png
processing resize:43.png
processing thumbnail:44.png
queue empty

代わりに while と書くと構文エラーになります。Go では while はただの識別子だからです。

example.texttext
./main.go:7:8: syntax error: unexpected name pending at end of statement

コンパイラは while を式として読み、その後ろに別の名前が続いているのを見つけて解析をあきらめます。メッセージはループについて何も触れていないので、ほかの言語から来た人は戸惑います。

無限ループ

節を 1 つも書かない for { ... } は、中の処理がループを抜けるまで実行を続けます。サーバーでは、接続を受け付けるループやポーリングにこの形を使います。

example.gogo
	for {
		conn, err := ln.Accept()
		if err != nil {
			if errors.Is(err, net.ErrClosed) {
				return
			}
			log.Printf("accept: %v", err)
			continue
		}
		go handle(conn)
	}

リスナーが閉じられて Accept が net.ErrClosed を返すと、ループは終わります。for {} は while (true) や for (;;) の代わりになり、コンパイラはこのループが自分では終わらないことを知っています。break のない for {} で終わる関数には、最後の return は必要ありません。

for range はどう動くのか?

range を使うと、for ループは値に対する反復になります。返ってくるものは型によって決まります。

range の対象1 つ目の値2 つ目の値
slice または配列インデックス要素のコピー
文字列バイトオフセットrune(Unicode コードポイント)
mapキー値
channel受信した値なし
整数 n(Go 1.22)0 から n-1なし
イテレーター関数(Go 1.23)yield された値yield された値

2 つ目の変数は省略でき(for i := range items)、1 つ目は _ で無視できます(for _, item := range items)。変数を書かない for range n は、ブロックを n 回実行します。

example.gogo
package main

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

func main() {
	endpoints := []string{"/health", "/orders", "/users"}
	for i, path := range endpoints {
		fmt.Println(i, path)
	}

	statusCounts := map[int]int{200: 812, 404: 17, 500: 3}
	for _, code := range slices.Sorted(maps.Keys(statusCounts)) {
		fmt.Printf("%d: %d\n", code, statusCounts[code])
	}

	for offset, r := range "café" {
		fmt.Printf("%d:%c ", offset, r)
	}
	fmt.Println()

	for attempt := range 3 {
		fmt.Print(attempt, " ")
	}
	fmt.Println()

	results := make(chan string, 2)
	results <- "job 1 done"
	results <- "job 2 done"
	close(results)
	for msg := range results {
		fmt.Println(msg)
	}
}
example.texttext
0 /health
1 /orders
2 /users
200: 812
404: 17
500: 3
0:c 1:a 2:f 3:é 
0 1 2 
job 1 done
job 2 done

注意が必要なのは map のループです。ランタイムは map の反復順序を意図的にランダム化していて、コードがその順序に頼れないようにしています。ステータスコードを順番に出力するために、maps.Keys がキーのイテレーターを返し、slices.Sorted がそれを集めてソートしています。map の反復については Go の map キーワードで詳しく解説しています。

文字列のループは rune を返し、オフセットはバイト位置です。そのため é はオフセット 3 から始まり、2 バイトを占めます。生のバイトが欲しいときは、for i := 0; i < len(s); i++ と s[i] を使います。

channel のループは、channel が閉じられると止まります。誰も閉じなければ、バッファが空になった時点でループは永久にブロックします。channel を誰が閉じるべきかは、Go の chan キーワードで解説しています。

イテレーター関数(Go 1.23)を使うと、シグネチャが func(yield func(K, V) bool) の関数なら何でも range で使えます。標準ライブラリでは maps.Keys、maps.All、slices.All、slices.Backward、strings.Lines がこれを返します。ページングされた API の結果やデータベースの行に対して、自分で書くこともできます。range のルールはここに収まりきらないほどあります。短くまとめたものは Go キーワード解説の range のセクションにあります。

Go 1.22 でループ変数の何が変わったのか?

Go 1.22 より前は、for ループは変数を 1 回だけ宣言し、すべての反復で使い回していました。その変数をキャプチャしたクロージャや goroutine は、クロージャが実行された時点の値を見ることになり、それはたいてい最後の値でした。Go 1.22 でこれが変わり、反復ごとに新しい変数が作られるようになりました(Go blog, Fixing For Loops in Go 1.22)。

次のヘルスチェッカーは、データベースのホストごとに goroutine を 1 つ起動します。

example.gogo
package main

import (
	"fmt"
	"sync"
)

func main() {
	hosts := []string{"db-1", "db-2", "db-3"}

	var wg sync.WaitGroup
	for _, host := range hosts {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println("checking", host)
		}()
	}
	wg.Wait()
}

go.mod が go 1.21 のとき、3 回実行するとどれも checking db-3 を 3 回出力し、go vet は次のように報告しました。

example.texttext
main.go:16:28: loop variable host captured by func literal

go.mod が go 1.27 なら、同じコードで 3 つのホストがすべてチェックされ(順序は goroutine が終わった順です)、go vet も何も報告しません。

この切り替えは、ビルドに使うツールチェーンではなく、go.mod の go 行で決まります。Go 1.27 のコンパイラでも、go 1.21 を宣言したモジュールは古いセマンティクスでビルドされます。そのため、1.22 より前に書かれた依存パッケージは、作者がテストしたときと同じように動き続けます。この変更は 3 つの節を持つ形と range の両方に適用されます。以前の回避策だったループ内の host := host は、もう必要ありません。go fix の forvar モダナイザーが自動で削除してくれます。クロージャとキャプチャされた変数については、Go の func キーワードで詳しく解説しています。

range の値を変更しても slice が変わらないのはなぜか?

range の 2 つ目の変数は要素のコピーです。struct の slice では、コピーのフィールドを変更しても slice はそのままです。

example.gogo
package main

import "fmt"

type Invoice struct {
	ID     string
	Amount int
	Paid   bool
}

func main() {
	invoices := []Invoice{
		{ID: "inv-101", Amount: 4200},
		{ID: "inv-102", Amount: 1800},
	}

	for _, inv := range invoices {
		inv.Paid = true
	}
	fmt.Println(invoices[0].Paid, invoices[1].Paid)

	for i := range invoices {
		invoices[i].Paid = true
	}
	fmt.Println(invoices[0].Paid, invoices[1].Paid)
}
example.texttext
false false
true true

1 つ目のループは、各反復の終わりに捨てられるコピーの Paid を設定しています。2 つ目のループは slice にインデックスでアクセスし、本物の要素を変更しています。Go 1.22 でもこの動きは変わっていません。1.22 の修正で反復ごとに専用の変数が作られるようになりましたが、その変数もやはりコピーです。ポインタの slice([]*Invoice)なら、コピーされるのはポインタなので、inv.Paid = true で請求書が更新されます。

要素へのポインタを保持する

&invoices[i] を取ると、slice の元になっている配列を指すポインタが得られます。後で slice が容量を超えて伸びると、このポインタは古いものになります。append が要素を新しい配列にコピーするからです。

example.gogo
	var large *Invoice
	for i := range invoices {
		if invoices[i].Amount > 4000 {
			large = &invoices[i]
		}
	}

	invoices = append(invoices, Invoice{"inv-103", 900})
	large.Amount = 0
	fmt.Println(invoices[0].Amount, large.Amount)
example.texttext
4200 0

large は古い配列を指したままなので、書き込みは invoices に届きません。slice が伸びる可能性があるなら、ポインタではなくインデックス(largeIdx = i)を保持します。

for ループで break と continue はどう動くのか?

break は最も内側の for、switch、select を抜けます。continue は最も内側の for の次の反復に進みます。どちらも、外側のループを指すラベルを任意で指定できます。

ラベルは識別子の後ろにコロンを付けたもので、ループの前の行に置きます。次のスキャンは、複数のログファイルを通じて最初のエラーが見つかった時点で止まります。

example.gogo
package main

import "fmt"

func main() {
	logLines := [][]string{
		{"INFO start", "INFO ready"},
		{"INFO request", "ERROR db timeout", "INFO retry"},
		{"INFO shutdown"},
	}

files:
	for file, lines := range logLines {
		for _, line := range lines {
			if line[:5] == "ERROR" {
				fmt.Printf("file %d: first error %q\n", file, line)
				break files
			}
		}
	}
}
example.texttext
file 1: first error "ERROR db timeout"

ラベルがなければ、break は内側のループを抜けるだけで、スキャンは 3 つ目のファイルに進みます。continue files と書けば、現在のファイルの残りを飛ばして次のファイルに進みます。

select の中の break

このバグはコンパイルが通り、ざっと読むと正しく見えます。次の worker は、context がキャンセルされるまでジョブを読み続けます。

example.gogo
func worker(ctx context.Context, jobs <-chan string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("worker stopping:", ctx.Err())
			break
		case job := <-jobs:
			fmt.Println("handled", job)
		}
	}
	fmt.Println("worker flushed metrics")
}

この break が抜けるのは for ではなく select です。worker は worker stopping を出力するとすぐに select に戻ります。ctx.Done() は閉じられたままなので、同じ行を休みなく出力し続けます。メトリクスは一度もフラッシュされません。go vet は、ループの後にある到達不能なコードを報告します。

example.texttext
main.go:20:2: unreachable code

staticcheck は break そのものを報告します。

example.texttext
main.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

直すには、ループにラベルを付けます。

example.gogo
func worker(ctx context.Context, jobs <-chan string) {
loop:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("worker stopping:", ctx.Err())
			break loop
		case job := <-jobs:
			fmt.Println("handled", job)
		}
	}
	fmt.Println("worker flushed metrics")
}
example.texttext
handled email:1
handled email:2
worker stopping: context deadline exceeded
worker flushed metrics

ループの後にすることがなければ、ctx.Done() の case で return しても構いません。ループの中の switch で break を使う場合も、同じ落とし穴があります。

for ループでよくある間違い

ループの中の defer

defer が実行されるのは、反復が終わったときではなく、関数が return するときです。ファイルを開いて Close を defer するループは、関数全体が終わるまですべてのファイルを開いたままにします。

example.gogo
	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			return 0, err
		}
		defer f.Close()
		// ...
	}

パスが数個なら問題は表に出ませんが、ログファイルが 1 万個あるとプロセスのファイルディスクリプタが尽きます。go vet も staticcheck もこれを検出しません。ループの本体を関数に切り出して各 defer がその呼び出しの終わりに実行されるようにするか、反復の最後で f.Close() を直接呼び出します。Go でよくある間違いと避け方では、この間違いを完全な例とともに扱っています。

range で反復しながら slice に append する

range はオペランドを最初の反復の前に 1 回だけ評価します。ループの中で append しても、ループの回数は増えません。

example.gogo
	urls := []string{"/", "/about"}
	for _, u := range urls {
		urls = append(urls, u+"/sitemap.xml")
	}
	fmt.Println(len(urls), urls)
example.texttext
4 [/ /about //sitemap.xml /about/sitemap.xml]

ループは元の要素ごとに 1 回ずつ、計 2 回実行されました。安全ではありますが、クローラーがこの方法で新しい URL を見つけられないということでもあります。処理しながら作業リストが増えていく場合は、上で紹介した for len(queue) > 0 のように、キューに対して条件だけのループを使います。

range の途中で map を変更する

map を range で反復している最中に、現在のキーを削除するのは許されていて、安全です。次のループは、アイドル状態のセッションを削除します。

example.gogo
	for id, idle := range sessions {
		if idle > 30 {
			delete(sessions, id)
		}
	}

ループ中にキーを追加することも許されていますが、仕様では新しいエントリーは「反復中に生成されることもあれば、スキップされることもある」("may be produced during the iteration or may be skipped")とされています。追加したキーをすべて処理する必要があるなら、新しいキーは別の map に集め、ループの後でマージします。

ループ変数のアドレスを取る

for _, inv := range invoices の中で ptrs = append(ptrs, &inv) とすると、得られるのは slice の要素ではなくコピーへのポインタです。Go 1.22 より前は、すべてのポインタが同じ変数を指し、最後の請求書を示していました。Go 1.22 以降は各ポインタがそれぞれのコピーを指すので正しく動きますが、slice とはつながっていません。元の要素を変更したいなら &invoices[i] を使い、上で説明した容量の注意点も頭に入れておきます。

Go に while ループや do-while ループはあるか?

ありません。Effective Go によると、Go の for ループは「for と while を統合したもので、do-while はない」("unifies for and while and there is no do-while")とされています。ほかの言語で while を使う場面は、条件だけの形でまかなえます。

do-while は、条件を調べる前に本体を 1 回実行します。Go では、最後にチェックを置いた無限ループとして書きます。よくあるのはページングされた API の呼び出しで、最初のページは必ず取得するからです。

example.gogo
	cursor := ""
	for {
		page := fetchPage(cursor)
		fmt.Println("fetched", page.Items)
		cursor = page.Next
		if cursor == "" {
			break
		}
	}
example.texttext
fetched [order-1 order-2]
fetched [order-3]

do { } while (cursor != "") と書く場合より 2 行長くなりますが、終了条件は探すときに目が向く一番下にあります。

LevelUpGo で学ぶ

LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Go Basics には for に関するレッスンが 2 つあり、1 つは 3 つのループの形、もう 1 つは range、break、continue を扱います。Concurrency Fundamentals では、ループの中で goroutine を起動する、channel を range で反復する、キャンセル時に for/select ループを抜ける、といった並行処理のコードでつまずきやすいループを扱います。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。

よくある質問

Go に while ループはありますか?

ありません。Go に while キーワードはありません。while ループは for cond { ... }、無限ループは for { ... } と書きます。while と書くと、コンパイラはそれをただの名前として扱うので、構文エラーになります。

Go に foreach ループはありますか?

for ... range が Go の foreach です。for _, v := range items は slice のすべての要素を順に処理し、同じ形は配列、map、文字列、channel、整数、イテレーター関数にも使えます。

Go で N 回ループするには?

Go 1.22 以降では、for i := range n で i が 0 から n-1 まで変化し、for range n なら変数なしで n 回実行されます。3 つの節を持つ for i := 0; i < n; i++ は、どのバージョンでも使えます。

Go で map をソートされた順に反復するには?

キーを集めてソートし、それを range で反復します。Go 1.23 以降は、for _, k := range slices.Sorted(maps.Keys(m)) で両方を 1 行で書けます。map の反復順序は意図的にランダム化されているので、決して依存しないでください。

Go で slice を逆順にループするには?

for i := len(s) - 1; i >= 0; i-- を使います。Go 1.23 以降なら for i, v := range slices.Backward(s) も使え、最後の要素から最初の要素に向かってインデックスと値を返します。

for ループの中の goroutine が同じ値を出力するのはなぜですか?

モジュールが 1.22 より前の Go バージョンになっていて、ループ変数がすべての反復で共有されているからです。go.mod の go 行を go 1.22 以上に変更すると、反復ごとに専用の変数が作られます。古いバージョンのままなら、値を引数として渡す(go func(h string) { ... }(host))か、ループの中で host := host とコピーします。

出典

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成