Goのイテレータは、Go 1.23.0(2024年8月13日リリース)で言語仕様と標準ライブラリの両方に正式に入った機能です。for range に関数を渡せるようになり、スライスやマップ以外のデータ構造も同じ書き方でたどれるようになりました。この記事では、range over funcの動作規則、iter パッケージの Seq/Pull、スライスを一定サイズに切り出す slices.Chunk の挙動を、公式ドキュメントと標準ライブラリのソースに当たりながら整理します。
まとめ
- イテレータの正体は
func(yield func(V) bool)という関数です。for rangeはこの関数を1回だけ呼び、yieldが呼ばれた回数だけループ本体が回ります。 yieldの戻り値がループ制御そのものです。ループ本体がbreakなどで終了するとyieldはfalseを返し、以後の呼び出しはpanicします。slices.Chunkの戻り値は[][]Tではなくiter.Seq[Slice]です。スライスとしてまとめて欲しい場合はslices.Collectと組み合わせます。- 標準ライブラリのイテレータ対応は Go 1.23 の
slices/mapsから始まり、1.24 のstrings/bytes、1.26 のreflectへと段階的に広がりました。 MapやFilterといった汎用アダプタを標準で用意する提案(x/exp/xiter)は2025年5月21日に却下されています。当面は自分で書くか、素直なforループを選ぶ判断になります。
英語圏の資料では range over func や Go iterator と表記され、検索するときは golang iter といった綴りでも同じ内容にたどり着けます。以下、仕様と実際のコードで順に確認します。
range over funcでfor-rangeに渡せるイテレータの仕組み
Go 1.23の言語仕様変更は「for range の右辺に関数を書ける」という一点に集約されます。イテレータ用の新しい構文やキーワードは追加されていません。
for-rangeが受け取る3つの関数型
言語仕様が受け付ける関数型は次の3つだけです。それ以外のシグネチャを渡すとコンパイルエラーになります。
| 関数型 | ループ変数 | 対応する標準API例 |
|---|---|---|
| func(func() bool) | なし | (自作の無限列など) |
| func(func(V) bool) | 1つ | slices.Values / maps.Keys |
| func(func(K, V) bool) | 2つ | slices.All / maps.All |
コールバック引数は慣習的に yield と名付けますが、これは予約語ではなく通常の識別子です。次のコードは3から1まで降順に値を渡す最小のイテレータです。
package main
import "fmt"
// Countdown は n から 1 まで降順に値を渡すイテレータを返します。
func Countdown(n int) func(func(int) bool) {
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)
}
}
yieldの戻り値とループ制御の対応
ここがrange over funcで最もつまずきやすい部分です。言語仕様は「ループ本体が(break 文などで)終了した場合、yield は false を返し、二度と呼び出してはならない」と定めています。iter パッケージのドキュメントはさらに踏み込んで「yield は false を返した後に呼ばれるとpanicする」と明記しています。
つまり if !yield(i) { return } は行儀の良い書き方ではなく、省略するとランタイムpanicを招く必須のガードです。先ほどの Countdown の呼び出し側に break を足しても安全に止まるのは、このガードがあるからです。
for v := range Countdown(3) {
fmt.Println(v)
if v == 2 {
break // ここで yield が false を返し、Countdown 側の return に入る
}
}
// 出力: 3, 2
ループ本体の return や goto でも同じく yield は false を返します。イテレータ関数の側から見ると、外側の制御構文は「yield の戻り値」という形でしか観測できません。
Go 1.22の試験提供から1.23の正式化までの経緯
この機能はGo 1.22.0(2024年2月6日リリース)で先行提供されており、リリースノートには「GOEXPERIMENT=rangefunc を付けてビルドするとこの機能が有効になる」と書かれていました。6か月後の Go 1.23.0 で環境変数なしの標準機能に昇格しています。
したがって、range over funcを使うコードは go.mod の go ディレクティブが 1.23 以上である必要があります。プロジェクトごとにGoのバージョンを切り替えたい場合は、GOTOOLCHAINによるGoバージョンの自動切り替えを使うと go.mod の記述に応じたツールチェーンが自動で選ばれます。
iterパッケージのSeq・Seq2とプル型イテレータ
iter パッケージ自体は非常に小さく、2つの型と2つの関数しか公開していません。言語仕様が受け付ける関数型に読みやすい名前を与え、プル型への変換手段を1つ提供するだけの薄い層です。
Seq・Seq2の型定義と命名規約
型定義はそのまま、前節の関数型のエイリアスです。
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
自作の型でイテレータを提供するときは、標準ライブラリが採用した命名にそろえると読み手が迷いません。All は全要素(Seq2 でインデックスや鍵と組にする)、Values は値のみ、Backward(slices のみに存在)は逆順、というのが実際に使われている割り当てです。Go公式ブログ「Range Over Function Types」(Ian Lance Taylor、2024年8月20日)は、コンテナ型には All メソッドを用意することを推奨しています。
import "iter"
// Values は s の要素を順に渡すイテレータを返します。
func Values[E any](s []E) iter.Seq[E] {
return func(yield func(E) bool) {
for _, v := range s {
if !yield(v) {
return
}
}
}
}
iter.Pullによる値の取り出しとstopの呼び出し義務
ここまでのイテレータは、値を受け手に押し付けるプッシュ型です。2つの列を突き合わせるような処理では、必要なタイミングで1件ずつ引き出すプル型のほうが書きやすくなります。iter.Pull はプッシュ型をプル型に変換します(以下は関数内に置く前提の抜粋です)。
next, stop := iter.Pull(slices.Values([]int{10, 20, 30}))
defer stop()
for {
v, ok := next()
if !ok {
break
}
fmt.Println(v)
}
stop の呼び出しは省略できません。公式ドキュメントは「次の値が不要になり、かつ next がまだ終端(false)を返していない時点で stop を呼ばなければならない。通常は defer stop() とするとよい」としています。stop は複数回呼んでも、next が既に false を返した後に呼んでも安全です。
実装を知っておくと挙動を読み違えずに済みます。標準ライブラリのソース(src/iter/iter.go)は runtime.newcoro と runtime.coroswitch を linkname で参照しています。newcoro はgoroutineを1本生成しますが(src/runtime/coro.go のコメントは「fの実行を待って停止しているgoroutineを含むcoroを作る」と説明しています)、スケジューラを経由せず coroswitch で制御を直接受け渡すため、両者が並行に走ることはありません。ドキュメントも「複数のgoroutineから next や stop を同時に呼ぶのは誤り」と明記しているので、ゴルーチンによる並行処理と組み合わせる場合は呼び出しを1つのgoroutineに閉じ込めてください。
slices.Chunkによるスライス分割の実装と落とし穴
slices.Chunk はGo 1.23.0で追加された関数で、スライスを指定サイズごとの連続した部分スライスに切り出します。バルクAPIへの分割送信やページング処理のように、一定件数ずつまとめて処理したい場面で使います。
戻り値は[][]Tではなくiter.Seq
シグネチャは次のとおりです。
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]
返るのはイテレータであって [][]T ではありません。fmt.Println(slices.Chunk(ids, 3)) と書いても関数値のアドレスが出るだけで、コンパイルエラーにもならないため気づきにくい取り違えです。実際の使い方を確認しておきます。
package main
import (
"fmt"
"slices"
)
func main() {
ids := []int{1, 2, 3, 4, 5, 6, 7}
for c := range slices.Chunk(ids, 3) {
fmt.Println(c)
}
// 出力: [1 2 3] 、[4 5 6] 、[7]
// まとめて [][]int が欲しい場合は Collect と組み合わせます
all := slices.Collect(slices.Chunk(ids, 3))
fmt.Println(len(all)) // 3
}
nの下限と端数チャンクの挙動
公式ドキュメントは「n が1未満の場合 Chunk はpanicする」と定めています。分割サイズを設定値や外部入力から受け取る設計では、呼び出し前の検証が必須です。実装(src/slices/iter.go)でも関数の冒頭で panic("cannot be less than 1") が実行されます。
要素数が n で割り切れない場合、最後のチャンクだけが短くなります。元のスライスが空のときは、空のチャンクが1つ返るのではなく、シーケンス自体が空です。つまりループ本体が一度も実行されません。0件の入力を正常系として扱うのか異常として扱うのかを、Go言語のエラーハンドリングの方針に沿って呼び出し側で明示しておかないと、後続処理が無言でスキップされる検知しにくい不具合になります。
元スライスとの配列共有によるバッチ処理の注意点
ドキュメントの「すべての部分スライスは長さを超える容量を持たないよう切り詰められる」という記述は、実装を見ると意味がはっきりします。Chunk は yield(s[i : i+end : i+end]) という3インデックススライスを渡しており、容量を長さと同じに詰めています。
この結果、チャンクに append しても新しい配列が確保されるため元のスライスは汚れません。一方で、チャンクの要素そのものを書き換えると元のスライスに波及します。同じ配列を指しているためです。
ids := []int{1, 2, 3, 4}
for c := range slices.Chunk(ids, 2) {
c[0] = 0 // 元の ids も書き換わります
_ = append(c, 99) // cap が詰めてあるため ids には影響しません
}
fmt.Println(ids) // [0 2 0 4]
チャンクを別のgoroutineへ渡して並行に加工するようなバッチ処理では、この共有が競合の原因になります。書き換えを伴う場合は slices.Clone で複製してから渡してください。
標準ライブラリのイテレータ対応の広がり
Go 1.23の追加分だけを見ていると取りこぼしますが、標準ライブラリ側のイテレータ対応はその後も版を追って拡張されています。どのバージョンで何が使えるかを整理しておきます。
| バージョン | リリース日 | イテレータ関連の追加 |
|---|---|---|
| 1.22 | 2024-02-06 | GOEXPERIMENT=rangefunc で先行提供 |
| 1.23 | 2024-08-13 | iter新設、slices 9関数、maps 5関数 |
| 1.24 | 2025-02-11 | strings・bytes に各5関数 |
| 1.25 | 2025-08-12 | リリースノートに記載なし |
| 1.26 | 2026-02-10 | reflect の Type.Fields ほか6メソッド |
Go 1.24で strings と bytes に加わった Lines、SplitSeq、SplitAfterSeq、FieldsSeq、FieldsFuncSeq は、実務での効き目が大きい追加です。従来の strings.Split は結果のスライスを一括で確保しますが、SplitSeq は必要な分だけ返すため、巨大なテキストを行単位で処理する場面で確保するメモリを抑えられます。Go 1.26では reflect.Type.Fields/Type.Methods/Type.Ins/Type.Outs と Value.Fields/Value.Methods が追加され、NumField と添字ループで書いていたリフレクション処理が for range で書けるようになりました。なお本稿執筆時点(2026年8月)の安定版は go1.26.5 で、Go 1.27 は go1.27rc2 のリリース候補段階です。iter パッケージ自体への関数追加は1.27でも行われていません。最新版の確認方法と移行手順はGo 1.27の新機能・変更点まとめで扱っています。
x/exp/xiter却下後のMap・Filterの書き方
イテレータが入った直後から、Map/Filter/Limit といった汎用アダプタを標準で提供する提案(golang/go#61898「x/exp/xiter: new package with iterator adapters」)が議論されてきました。この提案は2025年5月21日に提案審査グループによって declined as retracted(GitHub上のステータスは not planned)としてクローズされています。x/exp/xiter というパッケージは公開されておらず、pkg.go.devでも404になります。
クローズ時のコメントでaclements氏は「Goにおけるイテレータはまだ日が浅く、経験を積む余地が大きい。特定のアダプタが広く使われるようになったら、個別の提案を出してほしい」と述べています。さらに関連issue #76481では、この提案と重なる新規提案はクローズから2年間(2027年5月まで)は歓迎されず、個人の経験談ではなくエコシステム全体の利用統計を添えることが期待される、という補足が示されました。
実務上の結論ははっきりしています。標準の汎用アダプタを待つ設計にしてはいけません。2〜3行で済む変換なら、自前のヘルパーを1つ定義するのが最も摩擦が少ない選択です。
// Map は seq の各要素に f を適用したイテレータを返します。
func Map[A, B any](seq iter.Seq[A], f func(A) B) iter.Seq[B] {
return func(yield func(B) bool) {
for v := range seq {
if !yield(f(v)) {
return
}
}
}
}
アダプタを何段も重ねるパイプラインを組みたい場合は、標準採用の見込みが立っていない以上、サードパーティ製ライブラリへの依存になります。共通ヘルパーをどこに置くかはStandard Project Layoutに沿ったディレクトリ構成の判断と併せて決めておくと、後からの移動を避けられます。
イテレータを書かないほうがよい場面
新機能は「使えるところすべてで使う」対象になりがちですが、この件については設計者自身が抑制的です。Go公式ブログでIan Lance Taylor氏は、イテレータで書ける処理について「多くの場合はループのほうが分かりやすい。全員がいつもこのスタイルでコードを書くよう促したいわけではない」と書いています。
実際、次の条件に当てはまるならイテレータ化は見送るべきです。
- 対象が既にスライスかマップである場合。
for i, v := range sで足りるところをslices.Allに置き換えても、読み手が追う関数が1つ増えるだけです。 - 反復のたびにエラーを返しうる場合。
iter.Seq2[T, error]は書けますが、呼び出し側で毎回エラーを判定する分岐が入り、通常のforループより冗長になります。#61898の議論でも、この形の扱いにくさが繰り返し指摘されました。 - 反復の途中で外部から進行を制御したい場合。プッシュ型のイテレータは制御を関数側に渡すため、
iter.Pullを挟む一手間が発生します。
逆に効果がはっきりするのは、木構造やリンクリストのように「たどり方をカプセル化したいが、中間スライスを作りたくない」ケース、そして slices.Chunk や strings.SplitSeq のように全体を確保せず必要な分だけ切り出すケースです。イテレータの価値は抽象化そのものではなく、この確保の回避にあります。
よくある質問
Goのイテレータはどのバージョンから使えますか
環境変数なしで使えるのはGo 1.23.0(go1.23、2024年8月13日リリース)以降です。Go 1.22.0では GOEXPERIMENT=rangefunc を付けてビルドした場合に限り利用できました。実際に使うには go.mod の go ディレクティブを 1.23 以上にする必要があり、これより古い指定のままだと range over func はコンパイルエラーになります。CI環境とローカルでツールチェーンがずれやすい箇所なので、GOTOOLCHAINの設定も併せて確認してください。
slices.Chunkの結果を[][]Tとして受け取るにはどうしますか
slices.Collect(slices.Chunk(s, n)) と組み合わせます。Chunk の戻り値は iter.Seq[Slice] なので、そのまま [][]T の変数へ代入することはできません。ただし Collect を挟むと全チャンクを一度にメモリへ確保するため、確保量を抑えるというイテレータ本来の利点が失われます。バルクAPIへの分割送信のように1チャンクずつ処理して捨てられる用途では、Collect を挟まず for range で回すほうが適しています。
breakでループを抜けた後にyieldを呼ぶとどうなりますか
panicします。言語仕様は「ループ本体が終了した場合、yield は false を返し、二度と呼び出してはならない」と定めており、iter パッケージのドキュメントも「false を返した後に呼ばれるとpanicする」と明記しています。イテレータ関数側では if !yield(v) { return } の形で必ず戻り値を判定してください。判定を省いたイテレータは、呼び出し側が break しない限り正常に動くため、テストをすり抜けやすい不具合になります。
iter.Pullでstopを呼び忘れるとどうなりますか
反復が終端まで進んでいない状態で stop を呼ばずに抜けると、中断されたコルーチンとその実体であるgoroutineが解放されないまま残ります。next が false を返し切っていれば解放されますが、途中で break や return する可能性がある以上、defer stop() を常に書くのが安全です。stop は複数回呼んでも、next が既に false を返した後に呼んでも問題ないよう設計されています。
MapやFilterは標準ライブラリにありますか
ありません。それらを提供する x/exp/xiter の提案は2025年5月21日にクローズされ、重なる新規提案も2027年5月までは受け付けない方針が示されています。go1.27rc2 時点でも iter パッケージが公開しているのは Seq/Seq2 の2型と Pull/Pull2 の2関数だけです。必要な変換は自前のヘルパー関数として定義するか、素直な for ループで書いてください。