Goのイテレータは、for range に関数を渡して要素を1つずつ受け取る仕組みです。Go 1.23.0(2024年8月13日リリース)で言語仕様と標準ライブラリに正式に入り、現行の Go 1.27 系でも同じ書き方のまま使えます。この記事では、range over funcの動作規則、iter パッケージの Seq/Pull、スライスを一定件数ずつ切り出す slices.Chunk の挙動を、標準ライブラリのソースと go1.27.2 での実行結果に当たりながら整理します。
まとめ
- イテレータの正体は
func(yield func(V) bool)という関数です。for rangeはこの関数を1回だけ呼び、yieldが呼ばれた回数だけループ本体が回ります。 yieldがfalseを返した後にもう一度呼ぶとpanicします。if !yield(v) { return }は、次の要素へ進む前に反復停止を確認する基本形です。slices.Chunkの戻り値は[][]Tではなくiter.Seq[Slice]です。チャンクは元のスライスと配列を共有するので、要素の書き換えは元に波及します。- 10万件の合計をとる実測では、関数イテレータはスライスを作る方式と同程度の速さで確保0回、チャネル方式は約88倍、
iter.Pullは約30倍遅くなりました。 go fix(Go 1.26で刷新)はstrings.SplitのループをSplitSeqに、逆順ループをslices.Backwardに自動で書き換えます。Map/Filterを提供する x/exp/xiter の提案は2025年5月21日に取り下げ扱いで却下されており、標準の汎用アダプタは Go 1.27 時点でもありません。
range over funcでイテレータが動く仕組み
Go 1.23の言語仕様変更は「for range の右辺に関数を書ける」という一点に集約されます。イテレータ用の新しい構文やキーワードは追加されていません。英語圏の資料では range over func、Go iterator、golang iter といった表記で同じ機能を指します。
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 と名付けますが、予約語ではなく通常の識別子です。次のコードは n から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)
if v == 2 {
break // yield が false を返し、Countdown 側の return に入る
}
}
// 出力: 3、2
}
yieldの戻り値を無視したときのpanic
range over funcで最もつまずきやすいのが yield の戻り値です。Go言語仕様のfor文(range節)は、ループ本体が break や return で終わった場合は yield が false を返し、以後は呼び出してはならないと定めています。iterパッケージのドキュメント(src/iter/iter.go)も「Yield panics if called after it returns false.」と明記しています。
戻り値を見ずに2回呼ぶイテレータを書き、呼び出し側で1件目の直後に break すると、go1.27.2では次のメッセージでpanicしました。
func Bad(yield func(int) bool) {
yield(1)
yield(2) // break 後に呼ばれる
}
for v := range Bad {
fmt.Println(v)
break
}
// panic: runtime error: range function continued iteration
// after function for loop body returned false
このバグは呼び出し側が最後まで回す限り表に出ないため、イテレータには途中で break するテストを1本足しておくのが確実です。
go.modのgoディレクティブと使える版
この機能はGo 1.22.0(2024年2月6日)で GOEXPERIMENT=rangefunc を付けたビルドに限って先行提供され、Go 1.23.0で環境変数なしの標準機能になりました。range over funcを書くには go.mod の go ディレクティブが 1.23 以上である必要があります。strings.SplitSeq の利用にはGo 1.24以上のツールチェーンが必要です。go1.27.2で go 1.23 指定のモジュールに SplitSeq を書くと go build は通り、go vet だけが「strings.SplitSeq requires go1.24 or later (module is go1.23)」と報告しました。ビルドが通っても、使う関数に合わせて go ディレクティブを上げておく必要があります。手元のツールチェーンと go.mod の指定がずれる場合は、GOTOOLCHAINによるGoバージョンの自動切り替えで go.mod に合う版を自動で選ばせると管理が楽になります。
iter.Seq・Seq2による自作型のイテレータ実装
iter パッケージが公開しているのは、2つの型(Seq/Seq2)と2つの関数(Pull/Pull2)だけです。Go 1.27でもこの構成は変わっていません。
Seq・Seq2の型定義とAllメソッドの命名
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
型は前節の関数型に名前を付けただけです。Seq2 と同じ形の型引数は ジェネリクスの型パラメータで書かれており、Go 1.18以降の構文がそのまま使われています。命名は標準ライブラリにそろえると読み手が迷いません。全要素は All(slices.All はインデックスと値の Seq2)、値のみは Values、逆順は Backward です。Go公式ブログ「Range Over Function Types」(Ian Lance Taylor、2024年8月20日)は、すべてのコンテナ型に All メソッドを用意することを推奨しています。
木構造をたどる再帰イテレータ
イテレータの効果がはっきり出るのは、たどり方をカプセル化したいが中間スライスは作りたくないデータ構造です。二分木の中順走査は、再帰呼び出しの戻り値で「止まれ」を伝播させるのが要点です。
type Tree struct {
Left, Right *Tree
Val int
}
// All は木の値を左部分木・現在の節・右部分木の中順で渡します。
func (t *Tree) All() iter.Seq[int] {
return func(yield func(int) bool) { t.push(yield) }
}
func (t *Tree) push(yield func(int) bool) bool {
if t == nil {
return true
}
return t.Left.push(yield) && yield(t.Val) && t.Right.push(yield)
}
&& の短絡評価で、yield が false を返した瞬間に残りの部分木をたどらずに戻ります。値1・2・3の木で2の直後に break すると、出力は1、2で止まりpanicもしませんでした。戻り値を bool にせず再帰すると、右部分木で yield が再び呼ばれて前節のpanicになります。
エラーを返すSeq2[T, error]と使い切りのイテレータ
ファイルやネットワークから読むイテレータは、途中のエラーを iter.Seq2[T, error] の2つ目の値で返すのが定番です。
func Lines(sc *bufio.Scanner) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
for sc.Scan() {
if !yield(sc.Text(), nil) {
return
}
}
if err := sc.Err(); err != nil {
yield("", err)
}
}
}
呼び出し側は毎回 if err != nil を書くことになり、通常のループより冗長です。また、この実装は bufio.Scanner の読み位置を反復の間で共有します。最後まで読んだ後にもう一度 for range すると何も返さず(2行の入力で1回目2件、2回目0件)、途中で抜けた後なら残りの行から再開し、エラーで止まった後なら同じエラーをもう一度返します。iter のドキュメントはこれを「single-use iterator」と呼び、返す関数のドキュメントコメントに使い切りであることを書くよう求めています。エラー値の判定方法はGo言語のエラーハンドリング実装ガイドと同じ考え方で扱えます。
iter.Pullでプル型に変換する手順と注意点
ここまでのイテレータは値を受け手に押し付けるプッシュ型です。2つの列を1件ずつ突き合わせる処理では、必要なときに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 を呼ばなければならず、通常は defer stop() とするよう書いています。stop は複数回呼んでもよく、stop 後の next はゼロ値と false を返します(go1.27.2で 0 false を確認)。
実装はgoroutineを1本生成します。src/iter/iter.go は runtime.newcoro と runtime.coroswitch を参照しており、newcoro は f の実行を待つgoroutineを作ります。スケジューラを経由せず coroswitch で制御を直接受け渡すため両者が並行に走ることはありませんが、stop を呼ばずに抜けるとこのgoroutineが残ります。ドキュメントは複数のgoroutineから next や stop を同時に呼ぶことも誤りとしているので、ゴルーチンによる並行処理と組み合わせるときは呼び出しを1つのgoroutineに閉じ込めてください。
この切り替えにはコストがあります。後述の実測では、同じ10万件を for range で回す場合の約30倍の時間がかかりました。列を突き合わせるなど、必要なときに次の値を取り出す形が処理を簡潔にする場面で使い、性能が重要な箇所では実際の入力で比較してください。
slices.Chunkでスライスを分割する使い方
slices.Chunk はGo 1.23.0で追加された関数で、スライスを指定件数ごとの部分スライスに切り出します。バルクAPIへの分割送信やDBへの一括INSERTのように、一定件数ずつ処理したい場面で使います。
slices.Chunkの戻り値iter.Seqとスライス化
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]
返るのはイテレータです。fmt.Println(slices.Chunk(ids, 3)) と書くと関数値のアドレス(実行例では 0x10e2420)が出るだけで、go build も go vet も警告しませんでした。気づきにくい取り違えです。
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: cannot be less than 1になります。分割件数を設定値から受け取る設計では呼び出し前の検証が必須です。- 割り切れない場合は最後のチャンクだけ短くなります。
nが要素数より大きければ、全要素を含むチャンクが1つ返ります。 - 空スライスでは空のチャンクが1つ返るのではなく、シーケンス自体が空でループ本体が一度も実行されません。0件を異常として扱うなら呼び出し側で先に判定してください。
チャンク番号付きで回す書き方
Chunk は Seq なので、for i, c := range のように番号を受け取れません。番号が要るときは、Collect で [][]T にするより、ループの外に数を数える変数を置くほうが確保が増えません。
i := 0
for c := range slices.Chunk(ids, 3) {
fmt.Println(i, c) // 0 [1 2 3]、1 [4 5 6]、2 [7]
i++
}
10万件を100件ずつに分ける実測では、Chunk を直接回すと確保0回・約3.4マイクロ秒、Collect を挟むと11回・59,368バイトの確保で約14マイクロ秒でした。
元スライスとの配列共有
Chunk の実装(src/slices/iter.go)は 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がアクセスし、その少なくとも一方が書き込むとデータ競合になります。重ならないチャンクを各goroutineが専有して加工するだけなら、配列を共有していても直ちに競合するわけではありません。元スライスの要素への変更を分離したい場合は、渡す前に slices.Clone で複製してください。ただし複製は浅いため、要素に含まれるポインタ・マップ・スライスの参照先は共有されます。
チャネル・スライス・iter.Pullとの速度とメモリの比較
イテレータが入る前は、コールバック関数、次の値を返す関数、チャネルなどで列を1件ずつ渡すか、スライスを作って先に全件をそろえていました。0から99,999までの整数を生成して合計する処理を、4つの方式で比べた結果です。go test -bench . -benchmem -count 3 を go1.27.2 darwin/amd64(Intel Core i9-9880H)で実行し、時間は3回の中央値を載せています。チャネル方式は生成側のgoroutineからバッファなしチャネルへ1件ずつ送り、iter.Pull は関数イテレータを next で最後まで取り出す形です。
| 方式 | 1回あたりの時間 | 確保バイト | 確保回数 |
|---|---|---|---|
| 関数イテレータ(iter.Seq) | 約0.25ミリ秒 | 0 | 0 |
| スライスを作って返す | 約0.29ミリ秒 | 802,824 | 1 |
| iter.Pull で取り出す | 約7.8ミリ秒 | 224 | 7 |
| チャネル(goroutine) | 約22ミリ秒 | 約150 | 2 |
関数イテレータで1件ごとに増えるのは関数呼び出し1回だけですが、チャネル方式では送受信とgoroutineの切り替えが、iter.Pull ではコルーチンの切り替えが1件ごとに起きます。スライス方式は速度では互角でも、件数に比例したメモリ(10万件で約800KB)を先に確保します。
文字列の分割でも同じ傾向です。10,000行のテキストを strings.Split で回すと163,841バイトを1回確保して約0.15ミリ秒、strings.SplitSeq では確保0回で約0.12ミリ秒でした。イテレータの利点は抽象化そのものより、この確保の回避にあります。チャネルを列の生成器として使っている既存コードは、Goのchannelの挙動が必要な並行処理でない限り、関数イテレータへ置き換える価値があります。
標準ライブラリのイテレータ関数一覧
標準ライブラリ側のイテレータ対応は、1.23の slices/maps の後も版を追って増えています。次の表は Go のソースツリーにある api/go1.N.txt(各版で追加された公開APIの一覧)から、iter.Seq を返す・受け取るものを拾ったものです。
| 版(リリース日) | 主な追加 |
|---|---|
| 1.23(2024-08-13) | iter新設、slices 9関数、maps 5関数、reflect Value.Seq |
| 1.24(2025-02-11) | strings・bytes に Lines、SplitSeq ほか各5関数 |
| 1.25(2025-08-12) | 追加なし |
| 1.26(2026-02-10) | reflect の Type.Fields・Value.Methods ほか6メソッド |
| 1.27(2026-08-19) | encoding/json/jsontext の Pointer.Tokens |
| 1.28(開発中・2027年2月予定) | flag の FlagSet.All、regexp の All 系と SplitSeq |
1.23で入った slices の9関数は All、Values、Backward、Chunk、Collect、AppendSeq、Sorted、SortedFunc、SortedStableFunc、maps の5関数は All、Keys、Values、Collect、Insert です。マップのキーを順序付きで回す定番の slices.Sorted(maps.Keys(m)) は、この組み合わせで書けます。
実務での効き目が大きいのは1.24の strings.Lines と SplitSeq です。Lines は改行を含めたまま1行ずつ返す点に注意してください("x\ny" は "x\n" と "y" になります)。1.27は encoding/json/v2 が標準入りした版で、JSON Pointer の各要素を返す Pointer.Tokens が加わりました。1.28の項目は開発中のGo 1.28リリースノート(2026年11月1日時点)に載っているもので、正式版までに変わる可能性があります。最新版の確認方法と移行の注意点はGo 1.27の新機能・変更点まとめで扱っています。
go fixによる既存ループのイテレータへの書き換え
Go 1.26のリリースノートによると、go fix コマンドが作り直され、言語と標準ライブラリの新しい書き方へ自動で移行させる modernizer の置き場になりました。go1.27.2の go tool fix help には、イテレータ関連として次の4つが登録されています。
- stringsseq:
strings.Split/Fieldsをfor rangeで回すループをSplitSeq/FieldsSeqに置き換える(bytes も同様) - slicesbackward:添字を減らしていく逆順ループを
slices.Backwardに置き換える - mapsloop:マップを1件ずつコピーするループを
maps.Copy/Insert/Clone/Collectに置き換える - stditerators:標準ライブラリの
Len/At形式のループを、その型が持つイテレータに置き換える
3種類のループを含むファイルに go fix -diff ./... を実行すると、次の差分が出ました。
- for _, f := range strings.Split(csv, ",") {
+ for f := range strings.SplitSeq(csv, ",") {
- for k, v := range src {
- dst[k] = v
- }
+ maps.Copy(dst, src)
- for i := len(s) - 1; i >= 0; i-- {
- fmt.Println(s[i])
+ for _, v := range slices.Backward(s) {
+ fmt.Println(v)
書き換えは go.mod の go ディレクティブを見て判断されます。同じファイルで go 1.23 にすると SplitSeq(1.24で追加)への置き換えだけが出なくなり、go 1.22 では1件も出ませんでした。古い版を指定したモジュールに、使えない関数が書き込まれることはありません。適用前に -diff で差分を確認し、レビューしてから go fix ./... を実行する手順が安全です。
Map・Filterが標準にない理由とx/exp/xiterの却下
イテレータが入った直後から、Map/Filter/Limit などの汎用アダプタを提供する提案(golang/go#61898「x/exp/xiter: new package with iterator adapters」)が議論されてきました。この提案は2025年5月21日に提案審査グループが declined as retracted としてクローズしています(GitHub APIの state_reason は not_planned)。x/exp/xiter パッケージは公開されていません。
クローズ時のコメントでaclements氏は「Goのイテレータはまだ日が浅く、経験を積む余地が大きい。特定のアダプタがよく使われるようになったら、対象を絞った提案を出してほしい」と書いています。これを受けて2025年11月に Map などに絞った提案 #76481 が出ましたが、重複として閉じられました。議論の中でコントリビューターのseankhliao氏は、少なくとも2年の冷却期間と、自分のコードベースではなくエコシステム全体での利用実績を示す数字が要るという見解を述べています。これは提案審査グループの決定ではなく個人の見解ですが、近いうちに標準入りする見込みは低いと読めます。
実務上の結論ははっきりしています。標準の汎用アダプタを待つ設計にしてはいけません。必要な変換は数行のヘルパーとして自前で定義するのが最も摩擦の少ない選択です。
// 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
}
}
}
}
こうしたヘルパーをどのパッケージに置くかは、Goのディレクトリ構成の判断と併せて最初に決めておくと、後から移動せずに済みます。
イテレータを使わないほうがよい場面
Go公式ブログでIan Lance Taylor氏は、イテレータを連ねる書き方について「多くの場合はループのほうが分かりやすい。誰もがいつもこのスタイルで書くよう促したいわけではない」と書いています。次の条件に当てはまるならイテレータ化は見送るべきです。
- 対象が既にスライスかマップである。
for i, v := range sで足りるところをslices.Allに置き換えても、読み手が追う関数が1つ増えるだけです。 - 反復のたびにエラーを返しうる。
Seq2[T, error]は書けますが、呼び出し側に毎回エラー判定の分岐が入り、通常のループより長くなります。 - 列を並行に生産・消費したい。関数イテレータは呼び出し側と同じgoroutineで動くので、生産者を別goroutineで先に走らせたいならチャネルが適しています。
- 既にメモリ上にある2つのスライスを突き合わせるためだけに
iter.Pullを使う。添字で回すほうが読みやすく、前述の実測でもスライス方式はPullの約26分の1の時間でした。
逆に効果がはっきり出るのは、木構造やページングAPIのように「たどり方を隠したいが中間スライスは作りたくない」ケースと、slices.Chunk や strings.SplitSeq のように全体を確保せず必要な分だけ切り出すケースです。Goでの業務システム開発でこうした設計判断から相談したい場合は、フルスクラッチ開発の支援内容も参考にしてください。Goそのものの導入から始める場合はGo言語の実装入門が出発点になります。
よくある質問
Goのイテレータはどのバージョンから使えますか?
環境変数なしで使えるのはGo 1.23.0(2024年8月13日リリース)以降です。Go 1.22.0では GOEXPERIMENT=rangefunc を付けてビルドした場合に限り利用できました。go.mod の go ディレクティブが 1.23 未満のままだと range over func はコンパイルエラーになります。2026年11月時点の安定版は go1.27.3 で、書き方は1.23から変わっていません。
slices.Chunkの結果を[][]Tとして受け取るにはどうしますか?
slices.Collect(slices.Chunk(s, n)) と組み合わせます。そのまま [][]T の変数へ代入することはできません。Collect を挟むと全チャンクを入れるスライスを確保するため、10万件を100件ずつに分ける実測では確保が0回から11回(59,368バイト)に増えました。1チャンクずつ処理して捨てられる用途では for range で直接回すほうが適しています。
breakでループを抜けた後にyieldを呼ぶとどうなりますか?
panicします。go1.27.2では「range function continued iteration after function for loop body returned false」というメッセージでした。イテレータ関数側では if !yield(v) { return } などで停止要求を確認し、falseを返した後はyieldを再度呼ばないでください。呼び出し側が最後まで回す限り発生しないため、途中で break するテストを用意しておくと見逃しません。
iter.Pullでstopを呼び忘れるとどうなりますか?
終端まで進んでいない状態で stop を呼ばずに抜けると、Pull が内部で作ったgoroutineが解放されずに残ります。next が false を返し切っていれば解放されますが、途中で break や return する可能性がある以上、defer stop() を常に書くのが安全です。stop は複数回呼んでも問題ありません。
MapやFilterは標準ライブラリにありますか?
ありません。それらを提供する x/exp/xiter の提案は2025年5月21日に却下され、その後に出た Map などに絞った提案 #76481 も重複として閉じられています。Go 1.27時点で iter パッケージが公開しているのは Seq/Seq2 の2型と Pull/Pull2 の2関数だけです。必要な変換は自前のヘルパー関数として定義するか、素直な for ループで書いてください。