Goでイテレータといえば、チャネルか、インデックスを持ち回るforループでした。Go 1.23からrange over funcが安定し、自前の型でもfor rangeを回せます。標準のiterパッケージはGo 1.26.0(2026-02-10リリース)でも中心にあるので、その書き方を実機で確かめます。
range over funcが標準化した背景
Go 1.22までは、独自コレクションを走査させる標準的な手段がありませんでした。各ライブラリがNext()メソッドやコールバックを思い思いに用意し、呼び出し側は毎回作法を覚える必要がありました。
for rangeが関数を受け取れるようになった
Go 1.23から、for v := range fのfに特定のシグネチャを持つ関数を書けます。ループ本体が、その関数に渡されるyieldとして実行される仕組み。コレクション側は「値を1つ作ってはyieldに渡す」だけで、for rangeに組み込めます。
push型イテレータという考え方
公式のiterパッケージ概要では、この標準イテレータを“push iterators”と呼びます。
The standard iterators can be thought of as “push iterators”, which push values to the yield function.値を取りに行く(pull)のではなく、イテレータ側が
yieldへ値を押し込む(push)。データベースドライバのNext()がpull型だとすれば、こちらは制御が反転しています。
iter.Seqを自作する
// 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) {
return
}
}
}
}
func main() {
for v := range Countdown(3) {
fmt.Println(v)
}
}
実行結果。
3
2
1
Seqの正体はyieldを受け取る関数
iter.Seqの定義はシンプルです。型エイリアスではなく、ただの関数型。
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
戻り値をiter.Seq[int]と書くだけで、呼び出し側はfor rangeでそのまま回せます。ジェネリクスが効いているので、iter.Seq[string]でもiter.Seq[*User]でも同じ書き味。
yieldの戻り値でbreakに対応する
yieldが返すboolが肝です。ループ本体でbreakすると、yieldはfalseを返します。そこで走査を止めてreturnする責任は、イテレータ側にあります。上のCountdownがif !yield(i) { return }と書いているのはこのため。途中でbreakしたときに、無駄な値を作り続けないための合図です。
キー付きならSeq2を使う
インデックスやキーを一緒に返したいときはiter.Seq2。
// Enumerate は値にインデックスを添えて返す Seq2 イテレータ。
func Enumerate[V any](s []V) iter.Seq2[int, V] {
return func(yield func(int, V) bool) {
for i, v := range s {
if !yield(i, v) {
return
}
}
}
}
func main() {
words := []string{"go", "rust", "zig"}
for i, w := range Enumerate(words) {
fmt.Printf("%d=%s ", i, w)
}
fmt.Println()
}
0=go 1=rust 2=zig
slices・mapsの新関数と組み合わせる
自作だけでなく、標準ライブラリがすでにイテレータを返します。Go 1.23でslicesとmapsに追加された関数を押さえておくと、自前のforを書く場面が減ります。
| 関数 | 戻り値 | 用途 |
|---|---|---|
slices.Values(s) | iter.Seq[E] | スライスを値の列に変える |
slices.All(s) | iter.Seq2[int, E] | インデックス付きで走査 |
slices.Collect(seq) | []E | イテレータをスライスに集める |
slices.Sorted(seq) | []E | 集めてソートする |
maps.Keys(m) / maps.Values(m) | iter.Seq[K] / iter.Seq[V] | マップのキー・値を走査 |
マップのキーを安定した順序で回す
マップのfor rangeは順序が不定です。キー順で安定させたいとき、maps.Keysとslices.Sortedを繋げば1行で済みます。
m := map[string]int{"c": 3, "a": 1, "b": 2}
for _, k := range slices.Sorted(maps.Keys(m)) {
fmt.Printf("%s:%d ", k, m[k])
}
fmt.Println()
got := slices.Collect(maps.Values(m))
slices.Sort(got)
fmt.Println("values:", got)
a:1 b:2 c:3
values: [1 2 3]
インデックスを保ったまま絞り込む
slices.Allはインデックス付きのSeq2を返します。元の行番号を保ったまま条件で絞りたいときに効きます。
logs := []string{"INFO ok", "ERROR disk", "INFO done", "ERROR net"}
for i, line := range slices.All(logs) {
if line[:5] == "ERROR" {
fmt.Printf("L%d: %s\n", i, line)
}
}
L1: ERROR disk
L3: ERROR net
iter.Pullでpull型に変換する
for rangeが万能とは限りません。2本のイテレータを同時に1要素ずつ進めたいとき、push型のままでは書けません。for rangeは1本のループに制御を握られるからです。ここでpull型への変換が要ります。
2本の列を同時に進めたい場面
典型例がマージ処理。昇順の列AとBを1本の昇順列にまとめるには、両者の先頭を比べて小さいほうを取り、その列だけを1つ進める、を繰り返します。これはpush型のfor range2重ループでは表現できません。
Pullはnextとstopを返す
iter.Pullはpush型のイテレータを受け取り、pull型のインターフェースに変えます。
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
公式概要の説明はこうです。
Pull converts a standard push iterator to a “pull iterator”, which can be called to pull one value at a time from the sequence.
next()を呼ぶたびに値が1つ返り、列が尽きると2つ目の戻り値がfalseになります。これで2本を独立に進められます。
// Merge は2つの昇順イテレータを1本の昇順列にマージする。
func Merge(a, b iter.Seq[int]) iter.Seq[int] {
return func(yield func(int) bool) {
nextA, stopA := iter.Pull(a)
defer stopA()
nextB, stopB := iter.Pull(b)
defer stopB()
va, okA := nextA()
vb, okB := nextB()
for okA && okB {
if va <= vb {
if !yield(va) {
return
}
va, okA = nextA()
} else {
if !yield(vb) {
return
}
vb, okB = nextB()
}
}
for okA {
if !yield(va) {
return
}
va, okA = nextA()
}
for okB {
if !yield(vb) {
return
}
vb, okB = nextB()
}
}
}
func main() {
a := slices.Values([]int{1, 4, 7})
b := slices.Values([]int{2, 3, 8, 9})
fmt.Println(slices.Collect(Merge(a, b)))
}
[1 2 3 4 7 8 9]
stopを呼ばないとgoroutineがリークする
iter.Pullは内部でコルーチンを使い、push型の関数を途中で止められる状態に保ちます。列を最後まで読み切らずに処理を抜ける場合、stop()を呼ばないとその裏側の実行主体が宙づりのまま残ります。要はgoroutineリークと同じ構図。だからnextA, stopA := iter.Pull(a)の直後にdefer stopA()を置きます。最後まで読み切る場合でもdeferを付けておけば事故りません。リークの検出と原因追跡はGoのgoroutineリーク対策でpprofを使って掘り下げています。
イテレータでエラーをどう扱うか
push型イテレータはエラーの扱いで最初に詰まります。yieldのシグネチャはfunc(V) boolで、errorを返す口がありません。ファイル読み込みやネットワーク越しの走査で失敗を伝えたいとき、どうするか。
アンチパターン: errをpanicやログで握り潰す
エラーを返せないからと、イテレータ内でlog.Fatalしたりpanicに逃がす実装をたまに見ます。呼び出し側はエラーをハンドリングする機会を奪われ、ライブラリとしては使いものになりません。
Seq2[T, error]で値とエラーを一緒に流す
素直な解はiter.Seq2[T, error]。値とエラーをペアでyieldし、受け取った側がerr != nilを見てbreakします。
// Lines は io.Reader を1行ずつ返す。yield は error を返せないので
// (行, error) の Seq2 にして、エラーも値として流す。
func Lines(r *bufio.Reader) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
for {
line, err := r.ReadString('\n')
if line != "" {
if !yield(strings.TrimRight(line, "\n"), nil) {
return
}
}
if err != nil {
if err.Error() != "EOF" {
yield("", err)
}
return
}
}
}
}
func main() {
src := bufio.NewReader(strings.NewReader("alpha\nbravo\ncharlie"))
for line, err := range Lines(src) {
if err != nil {
fmt.Println("error:", err)
break
}
fmt.Println("read:", line)
}
}
read: alpha
read: bravo
read: charlie
並行処理の途中で出たエラーをまとめて扱いたいなら、イテレータの外側でerrgroupによるエラー集約と組み合わせる手もあります。
性能とハマりどころ
単純なループより遅い、ただしアロケーションはゼロ
1万要素の合計を、インデックスループ・自作イテレータ・slices.Valuesで測りました(Go 1.26.0, Apple M5, b.Loop()使用)。
BenchmarkIndexLoop-10 433773 2645 ns/op 0 B/op 0 allocs/op
BenchmarkRangeOverFunc-10 72420 16551 ns/op 0 B/op 0 allocs/op
BenchmarkSlicesValues-10 72452 16608 ns/op 0 B/op 0 allocs/op
自作イテレータはインデックスループの約6.3倍。yieldという関数呼び出しが要素ごとに挟まるためです。ただし両者とも0 allocs/opで、ヒープ確保は増えません。そして合計のような極小のループ本体だからこの差が目立つ点に注意。本体でI/Oや重い計算をすれば、相対的な差は縮みます。ホットパスの[]int合計をイテレータにする必要はありません。
yieldの戻り値を捨てるとpanicする
イテレータ自作でよくあるバグが、yieldの戻り値の無視です。break後も走査を続けると、ランタイムが弾きます。
// BAD: yield が false を返したのに return せず走査を続けている。
func BadCountdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i >= 1; i-- {
yield(i) // 戻り値を無視している
}
}
}
func main() {
for v := range BadCountdown(5) {
fmt.Println(v)
if v == 3 {
break
}
}
}
5
4
3
panic: runtime error: range function continued iteration after function for loop body returned false
breakでv == 3まで出た直後、4をyieldしようとして落ちます。メッセージが具体的なので原因はすぐ分かりますが、本番でpanicになる前にif !yield(...) { return }を徹底しておきます。
まとめ
range over funcはGo 1.23で安定し、Go 1.26でも標準の書き方。自前の型をfor rangeで回せるiter.Seq[V]はfunc(yield func(V) bool)という関数型。yieldの戻り値を見てreturnするのがイテレータ側の責任- キー付きは
iter.Seq2。slices.Values・maps.Keys・slices.Sortedを繋ぐと自前ループが減る - 2本の列を同時に進めるなら
iter.Pullでpull型に変換。stop()をdeferしないとリークする - エラーは
iter.Seq2[T, error]で値と一緒に流す。yieldはerrorを返せない - 性能はインデックスループの約6.3倍だが0 allocs。重い本体なら差は縮む

