主题
Go 迭代器:把“怎样取下一个”留给集合
一个目录里有十万个文件,调用者只想找到前三个符合条件的名字。如果 API 先返回一个大切片,它往往要先走完整个目录;如果 API 接收回调,调用者又得放弃熟悉的 for range。Go 的函数迭代器把这两件事接在了一起:提供者负责逐个产生值,使用者仍然写循环,并能用 break 叫停。range 遍历函数和标准库 iter 从 Go 1.23 开始可用。[1]
本文要讲清的是这个停止信号如何传回去,切片、map 怎样接上这个协议,以及读取文件一类有资源、有错误的过程该怎样收尾。示例在 Go 1.27.2、Linux/amd64 上运行;语言规则、标准库约定与本次输出会分别说明。
先认接口:迭代器究竟是什么类型
普通的 range xs 由语言知道如何遍历切片。函数迭代器则把这个动作交给一个函数:循环开始时调用它,并给它一个叫 yield 的回调。每次调用 yield(v),就执行一次循环体;回调返回 false 时,提供者必须停止。yield 是惯用参数名,不是关键字。[1:1]
语言接受三种函数形状,区别只在每次交出几个值:
| 迭代函数的形状 | 循环能接收的值 | 常见用途 |
|---|---|---|
func(func() bool) | 不接值 | 只通知发生了一次迭代 |
func(func(V) bool) | 一个值 | 依次交出名字、记录或数字 |
func(func(K, V) bool) | 两个值 | 下标与元素、键与值 |
iter 给后两种形状起了泛型类型名。把定义展开,协议就没有那么神秘了:[2]
go
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)它们不是包含游标字段的结构体,也没有强制的 Next 方法。迭代状态通常放在函数的局部变量或闭包里。类型名里的 2 只表示交出两个值,第二个值不一定是 map 的 value,也可以是下面会讲到的 error。
沿着一次 break,看清控制权怎样来回走
下面这个迭代器把动作打印出来。返回的函数真正开始运行时才打印 start,而不是调用 Count(4) 时就开始枚举。
go
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
fmt.Println("start")
defer fmt.Println("cleanup")
for i := 0; i < n; i++ {
fmt.Println("offer", i)
if !yield(i) {
fmt.Println("stop")
return
}
fmt.Println("resume", i)
}
}
}
seq := Count(4)
fmt.Println("created")
for n := range seq {
fmt.Println("body", n)
if n == 1 {
break
}
}
fmt.Println("loop ended")实际输出:
text
created
start
offer 0
body 0
resume 0
offer 1
body 1
stop
cleanup
loop ended读这段输出,最有用的是盯住 body 1 后面的变化:循环体要求退出,yield(1) 返回 false,迭代器随即 return,它自己的 defer 执行,外层循环才结束。没有生成 2 和 3,也没有先把四个数字存进切片。[1:2][2:1]
正在绘制图示…
查看图源代码
sequenceDiagram
participant R as for range
participant I as 迭代函数
participant B as 循环体
R->>I: 传入 yield,开始遍历
I->>B: yield(0)
B-->>I: true,继续
I->>B: yield(1)
B-->>I: false,循环体 break
I->>I: return,执行 defer
I-->>R: 迭代结束
这也解释了最重要的一条实现规则:每个可能继续生产数据的 yield 调用,都要处理它返回的布尔值。 忽略 false 后继续调用,就是违反协议。下面的反例已单独运行:
go
func bad(yield func(int) bool) {
yield(1)
yield(2) // 调用者已经 break,仍继续交出数据
}
for range bad {
break
}Go 1.27.2 检测到了它,panic 的内容为:
text
runtime error: range function continued iteration after function for loop body returned false错误文字属于这次实现的输出,不应拿来做跨版本业务判断。应该遵守的是语言协议:yield 返回 false 后不能再调用;迭代函数返回后也不能留着它供以后调用。不要把这个回调存到后台 goroutine 里,等循环结束后继续推送。[1:3]
惰性来自代码怎样写,不来自 Seq 这个名字
上面的 Count 在被遍历时才计算下一个数字,所以具有惰性。iter.Seq 本身只规定函数签名;如果函数先读完全部输入,再挨个 yield,它照样会提前消耗内存和时间。讨论“迭代器节省了什么”时,要看生产者是否真正按需工作。[2:2]
筛选是一个适合逐项完成的动作。下面的 Filter 接收一个迭代器,再返回一个迭代器:
go
func Filter[T any](src iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range src {
if keep(v) && !yield(v) {
return
}
}
}
}
src := slices.Values([]int{1, 2, 3, 4})
even := Filter(src, func(n int) bool { return n%2 == 0 })
fmt.Println(slices.Collect(even))实际输出:
text
[2 4]这里有两层停止传播。下游不想要更多值时,Filter 的 yield 返回 false;Filter 随即退出自己的 for range src,于是上游也收到 false。文末测试用一个计数器验证:输入为 0 到 9,保留偶数,拿到 2 就 break,上游只访问了 0、1、2 三个值,而且其清理函数已经执行。
筛选可以流式进行,排序则通常必须先收集全部数据。slices.Sorted(seq) 返回排好序的切片,调用完成前就得知道全部元素。不要把这些函数连在一起后,就认为整条链都不再分配或都支持无限输入。[3]
slices 与 maps:用现成入口接上自己的数据
先看返回类型,再看名字,会更容易选对函数。下面这些迭代相关入口从 Go 1.23 起进入标准库;slices 和 maps 包本身更早,在 Go 1.21 加入。[3:1][4][5]
| 数据方向 | 常用入口 | 得到什么 | 要留意什么 |
|---|---|---|---|
| 切片 → 值 | slices.Values(s) | iter.Seq[E] | 只给元素,不给下标 |
| 切片 → 下标、值 | slices.All(s) | iter.Seq2[int,E] | 下标从小到大 |
| 切片 → 倒序下标、值 | slices.Backward(s) | iter.Seq2[int,E] | 仍是原切片下标 |
| 迭代器 → 切片 | slices.Collect(seq) | 新切片 | 消费整个迭代器 |
| 迭代器 → 已有切片 | slices.AppendSeq(s,seq) | 追加后的切片 | 要接住返回值 |
| 迭代器 → 有序切片 | slices.Sorted(seq) | 排序后的新切片 | 需要消费所有输入 |
| map → 键、值、键值对 | maps.Keys、Values、All | Seq 或 Seq2 | 不承诺遍历顺序 |
| 键值对迭代器 → map | maps.Collect(seq) | 新 map | 同一键后面的值覆盖前面的值 |
| 键值对迭代器 → 已有 map | maps.Insert(m,seq) | 修改传入的 map | 目标必须能够接收写入 |
Seq2 的使用与遍历切片很接近:
go
for i, name := range slices.All([]string{"Ada", "Lin"}) {
fmt.Println("pair", i, name)
}
m := map[string]int{"b": 2, "a": 1}
fmt.Println("keys", slices.Sorted(maps.Keys(m)))
copied := maps.Collect(maps.All(m))
fmt.Println("map equal", maps.Equal(m, copied))实际输出:
text
pair 0 Ada
pair 1 Lin
keys [a b]
map equal truemaps.Keys 返回的不是 []string,因此不能直接对它做下标访问。需要切片就 Collect;需要稳定顺序就 Sorted。连续运行几次偶然拿到相同顺序,也不能证明 map 有序。无序是接口约定,不需要等到实验恰好出现顺序变化才成立。[4:1]
迭代器通常没有替你做快照
go
xs := []int{10, 20}
live := slices.Values(xs)
xs[0] = 99
fmt.Println("live", slices.Collect(live))实际输出:
text
live [99 20]slices.Values 保留着对那段切片数据的访问,创建它时并没有复制元素。若需要创建时的快照,应先明确复制,例如 slices.Values(slices.Clone(xs))。而 Clone 只做元素赋值;元素里还有切片、map、指针时,更深层的数据仍可能共享。[3:2]
同样,maps.All 不替 map 加锁。并发读写的同步责任仍在调用者,不能因为普通 range 换成函数迭代器,就去掉原来的锁。本文的 -race 验证只检查了附录中运行的路径,不是对所有迭代器实现的安全保证。[4:2][6]
Go 1.24 还加入了 strings.SplitSeq、FieldsSeq 等单次使用迭代器,bytes 也有对应入口。它们适合只想依次消费分段的场景;如果后面要随机访问或反复使用结果,收集成切片反而可能更清楚。这里的选择是 API 使用建议,不能代替针对真实输入的性能测试。[7]
读文件这样的过程,错误和资源该放在哪里
iter.Seq 没有内置错误通道。一个实用约定是返回 iter.Seq2[T,error]:正常项的错误为 nil;读取失败时交出错误,然后结束。这只是本例的 API 设计,不是语言替所有迭代器规定的格式。
go
// Lines 消费调用者给的 Reader,不负责回退或关闭它。
func Lines(r io.Reader) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
sc := bufio.NewScanner(r)
for sc.Scan() {
if !yield(sc.Text(), nil) {
return
}
}
if err := sc.Err(); err != nil {
yield("", err)
}
}
}
lines := Lines(strings.NewReader("first\nsecond\n"))
for line, err := range lines {
fmt.Println("line", line, err)
}
again := 0
for range lines {
again++
}
fmt.Println("second traversal", again)实际输出:
text
line first <nil>
line second <nil>
second traversal 0第二次没有内容,因为两次遍历消费的是同一个 Reader,第一遍已经读到结尾。迭代器能否重放必须由提供者说明,不能只看返回类型。iter 包文档专门区分可重复使用与单次使用的迭代器。[2:3]
这个实现还有三个边界:
Scanner有 token 大小限制。正常结束和扫描失败都会让Scan返回 false,必须检查Err。文末测试确实用 70,000 字节的一行触发了错误。需要长行时应设计可配置上限,或改用bufio.Reader。[8]- 调用者提前
break后,没再读取的内容是否损坏还不知道。不能声称“没有收到错误,就证明整个文件正常”。这由停止读取的行为直接得出。 - 这里 Reader 由调用者提供,因此也由调用者决定何时关闭。若换成
FileLines(path),由迭代器内部打开文件,打开动作和defer Close应放在返回的函数里:未遍历就不打开,退出时才有明确的关闭位置。关闭错误是否需要反馈,也要在 API 设计中约定。Seq不自动承担这些工作。[2:4][9]
需要手动拿“下一项”时,再用 iter.Pull
合并两路有序输入时,调用者往往需要交替拿两边的下一项。此时 for range 不容易直接表达,iter.Pull 可以把推送式的 Seq[V] 转成两个函数:next() (V,bool) 与 stop()。双值版本叫 Pull2。[10]
go
next, stop := iter.Pull(Count(3))
defer stop()
v, ok := next()
fmt.Println("next", v, ok)
stop()
v, ok = next()
fmt.Println("after stop", v, ok)与前面的 Count 搭配,实际输出为:
text
start
offer 0
next 0 true
stop
cleanup
after stop 0 false第一次 next 返回后,生产者停在第一次交出值的位置,它的 defer 还没有执行。调用 stop 后,生产者才能结束并清理。这就是为什么创建后应立即 defer stop():只消费一部分时,也必须给它结束机会。遍历已经耗尽或已经停止后,再调用 stop 是安全的;之后的 next 返回零值和 false。[10:1]
next 与 stop 不应该被多个 goroutine 同时调用。Pull 解决的是调用方式的转换,不是并发队列或线程安全包装。也没有必要为了统一写法,把每个普通 for range 都先转换一遍。[10:2]
取消需要显式检查,break 不会打断任意阻塞
前面的停止都是合作式的:生产者正在调用 yield,因此可以立即收到 false。若生产者卡在网络读取、数据库查询或自己写的无限计算里,外层 break 不能越过这段代码去中断它。要让任务响应取消,阻塞操作本身也要支持 context、截止时间或其他退出机制。[1:4][11]
对于纯计算,最小写法是在生成下一项之前检查 context:
go
func UntilCanceled(ctx context.Context, n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if ctx.Err() != nil {
return
}
if !yield(i) {
return
}
}
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range UntilCanceled(ctx, 100) {
fmt.Println("cancel demo", n)
cancel()
}实际只输出:
text
cancel demo 0本例用“静默结束”表示取消。如果调用者必须区分“正常读完”和“被取消”,应把错误纳入协议,例如返回 Seq2[T,error] 并交出 ctx.Err()。取消语义、重复使用、数据所有权这三件事,比给类型取个 Iterator 名字更影响调用者能否正确使用它。
版本检查与本次验证
安装了新工具链,不等于每个包都自动采用最新语言版本。附录的反例模块写着 go 1.22,在 Go 1.27.2 下仍无法编译函数 range:[12]
text
cannot range over func(yield func(int) bool) {…} ...:
requires go1.23 or later (-lang was set to go1.22; check go.mod)因此给旧项目引入迭代器时,要一起检查工具链、go.mod 的语言版本和使用的具体 API 的加入版本。不要只把 CI 的 Go 二进制换新。
2026-10-13 的实际验证包括:主程序输出、四个测试、go test -race、go vet、go 1.22 编译失败反例、违反 yield 协议的运行反例。正向测试全部通过;两个反例均出现预期结果。没有测量迭代器与切片的吞吐、分配或真实文件系统负载,所以本文不提供固定性能倍数。
参考资料
附录:全部复现文件与命令
保存为下面的目录。所有依赖都来自标准库,不需要下载模块。testdata 里的反例不会被普通的 go test ./... 当作包遍历,须按命令单独执行。
text
iteratorlab/
├── go.mod
├── main.go
├── main_test.go
└── testdata/
├── oldversion/{go.mod,main.go}
└── badprotocol/{go.mod,main.go}运行命令:
bash
go version # 本次为 go1.27.2 linux/amd64
go run .
go test -count=1 -v ./...
go test -count=1 -race ./...
go vet ./...
(cd testdata/oldversion && go build .) # 预期退出码 1
(cd testdata/badprotocol && go run .) # recover 打印协议 panicgo.mod
go.mod
module example.com/iteratorlab
go 1.27main.go
代码块 · 115 行 · 点击收起 / 展开
go
package main
import (
"bufio"
"context"
"fmt"
"io"
"iter"
"maps"
"slices"
"strings"
)
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
fmt.Println("start")
defer fmt.Println("cleanup")
for i := 0; i < n; i++ {
fmt.Println("offer", i)
if !yield(i) {
fmt.Println("stop")
return
}
fmt.Println("resume", i)
}
}
}
func Filter[T any](src iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range src {
if keep(v) && !yield(v) {
return
}
}
}
}
// Lines consumes r; it neither rewinds nor closes the caller's reader.
func Lines(r io.Reader) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
sc := bufio.NewScanner(r)
for sc.Scan() {
if !yield(sc.Text(), nil) {
return
}
}
if err := sc.Err(); err != nil {
yield("", err)
}
}
}
func UntilCanceled(ctx context.Context, n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if ctx.Err() != nil {
return
}
if !yield(i) {
return
}
}
}
}
func main() {
seq := Count(4)
fmt.Println("created")
for n := range seq {
fmt.Println("body", n)
if n == 1 {
break
}
}
fmt.Println("loop ended")
fmt.Println("even", slices.Collect(Filter(slices.Values([]int{1, 2, 3, 4}), func(n int) bool { return n%2 == 0 })))
for i, name := range slices.All([]string{"Ada", "Lin"}) {
fmt.Println("pair", i, name)
}
m := map[string]int{"b": 2, "a": 1}
fmt.Println("keys", slices.Sorted(maps.Keys(m)))
copied := maps.Collect(maps.All(m))
fmt.Println("map equal", maps.Equal(m, copied))
xs := []int{10, 20}
live := slices.Values(xs)
xs[0] = 99
fmt.Println("live", slices.Collect(live))
lines := Lines(strings.NewReader("first\nsecond\n"))
for line, err := range lines {
fmt.Println("line", line, err)
}
again := 0
for range lines {
again++
}
fmt.Println("second traversal", again)
next, stop := iter.Pull(Count(3))
defer stop()
v, ok := next()
fmt.Println("next", v, ok)
stop()
v, ok = next()
fmt.Println("after stop", v, ok)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range UntilCanceled(ctx, 100) {
fmt.Println("cancel demo", n)
cancel()
}
}main_test.go
代码块 · 77 行 · 点击收起 / 展开
go
package main
import (
"context"
"iter"
"slices"
"strings"
"testing"
)
func TestFilterStopsUpstream(t *testing.T) {
visited, closed := 0, false
src := iter.Seq[int](func(yield func(int) bool) {
defer func() { closed = true }()
for i := 0; i < 10; i++ {
visited++
if !yield(i) {
return
}
}
})
got := []int{}
for n := range Filter(src, func(n int) bool { return n%2 == 0 }) {
got = append(got, n)
if n == 2 {
break
}
}
if !slices.Equal(got, []int{0, 2}) || visited != 3 || !closed {
t.Fatalf("got=%v visited=%d closed=%v", got, visited, closed)
}
}
func TestPullCleanup(t *testing.T) {
closed := false
seq := iter.Seq[int](func(yield func(int) bool) {
defer func() { closed = true }()
yield(42)
})
next, stop := iter.Pull(seq)
defer stop()
if v, ok := next(); v != 42 || !ok {
t.Fatal(v, ok)
}
if closed {
t.Fatal("iterator already returned")
}
stop()
stop()
if !closed {
t.Fatal("missing cleanup")
}
if v, ok := next(); v != 0 || ok {
t.Fatal(v, ok)
}
}
func TestLinesError(t *testing.T) {
count := 0
for _, err := range Lines(strings.NewReader(strings.Repeat("x", 70_000))) {
count++
if err == nil {
t.Fatal("expected scanner error")
}
}
if count != 1 {
t.Fatal(count)
}
}
func TestCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
if got := slices.Collect(UntilCanceled(ctx, 10)); len(got) != 0 {
t.Fatal(got)
}
}testdata/oldversion/go.mod
go.mod
module example.com/olditerator
go 1.22testdata/oldversion/main.go
go
package main
func main() {
for range func(yield func(int) bool) { yield(1) } {
}
}testdata/badprotocol/go.mod
go.mod
module example.com/baditerator
go 1.27testdata/badprotocol/main.go
go
package main
import "fmt"
func bad(yield func(int) bool) { yield(1); yield(2) }
func main() {
defer func() { fmt.Println("panic:", recover()) }()
for range bad {
break
}
}Go 规范:For statements,包括函数 range 的三种签名、yield 与提前退出规则;Go 1.23 语言变化。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Go 1.27.2 iter 包,包括 Seq、Seq2、单次使用、标准库约定;本次也以同版本
go doc iter核对。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎Go 1.27.2 slices 包,
Values、All、Backward、Collect、AppendSeq、Sorted、Clone;固定源码 iter.go。 ↩︎ ↩︎ ↩︎Go 1.27.2 maps 包,
All、Keys、Values、Insert、Collect;固定源码 iter.go。 ↩︎ ↩︎ ↩︎Go 1.27.2 bufio.Scanner、
Err、Buffer、MaxScanTokenSize。 ↩︎Go 1.27.2 context 包:Done 与 Err 的契约,取消信号需要操作自行响应。 ↩︎