Go 1.23 range-over-func 迭代器实战:自定义可迭代类型、提前退出与惰性求值
在 Go 1.23 之前,想让自己的类型支持 for range,你要么把数据全塞进 slice 返回(内存全量拷贝),要么暴露一个 Next() (T, bool) 的手写迭代器(调用方要写一堆样板)。前者对大数据流不友好,后者不能用 for range 语法糖。
Go 1.23 正式落地了 range-over-func:只要你的函数签名符合 iter.Seq[V] 或 iter.Seq2[K,V],就能直接被 for range 遍历。这篇用几个能跑的例子把它讲透,包括最容易踩的「提前 break 后清理没执行」的坑。
先看朴素写法的痛点
假设要遍历一个链表,老写法只能这样:
go
type Node struct {
Val int
Next *Node
}
// 方案一:全量收集到 slice,链表很长时白白拷贝一份
func (n *Node) ToSlice() []int {
var out []int
for cur := n; cur != nil; cur = cur.Next {
out = append(out, cur.Val)
}
return out
}
for _, v := range list.ToSlice() { ... } // 调用方无感,但内存翻倍
如果链表有一千万个节点,ToSlice 会实打实分配一千万个 int 的空间。而很多时候你只是想找到第一个满足条件的元素就 break------全量拷贝纯属浪费。
range-over-func:惰性、不拷贝
iter.Seq[V] 的本质是这样一个类型:
go
// 标准库 iter 包里的定义
type Seq[V any] func(yield func(V) bool)
你写一个函数,它接收一个 yield 回调;每产出一个值就调用 yield(v),如果 yield 返回 false(调用方 break 了),你就停止。给链表加迭代器:
go
import "iter"
func (n *Node) All() iter.Seq[int] {
return func(yield func(int) bool) {
for cur := n; cur != nil; cur = cur.Next {
// yield 返回 false 说明调用方不想要了(break/return),立刻停
if !yield(cur.Val) {
return
}
}
}
}
调用方现在可以直接 for range,而且边遍历边产出,不预先拷贝:
go
for v := range list.All() {
if v > 100 {
break // yield 会返回 false,上面的迭代器函数收到后 return
}
fmt.Println(v)
}
break 之后,链表根本不会继续往后走。这就是惰性求值的价值。
带 key 的迭代器:iter.Seq2
需要同时产出下标或键值,用 iter.Seq2[K, V],yield 变成两个参数:
go
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
}
}
}
}
for i, v := range Enumerate([]string{"a", "b", "c"}) {
fmt.Printf("%d=%s\n", i, v) // 0=a 1=b 2=c
}
组合迭代器:像流水线一样串起来
迭代器最爽的地方是可组合。写一个通用的 Filter,它吃一个 Seq、吐一个 Seq,中间不产生任何中间 slice:
go
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq { // 遍历上游迭代器
if pred(v) && !yield(v) { // 命中条件才往下游 yield
return
}
}
}
}
// 只要链表里的偶数,全程零中间分配
for v := range Filter(list.All(), func(x int) bool { return x%2 == 0 }) {
fmt.Println(v)
}
Go 1.23 的标准库 slices、maps 也补了配套函数:slices.Collect(seq) 把迭代器收成 slice,maps.Keys(m) 直接返回 iter.Seq,slices.Sorted(maps.Keys(m)) 一行拿到排序后的 key。
最容易踩的坑:提前 break 时的资源清理
如果你的迭代器持有需要关闭的资源(文件、数据库游标),千万别只在循环正常结束后 Close------调用方可能 break。正确做法是用 defer:
go
func LinesOf(path string) iter.Seq[string] {
return func(yield func(string) bool) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close() // 关键:无论 yield 返回 false 提前退出,还是读完,都会关
sc := bufio.NewScanner(f)
for sc.Scan() {
if !yield(sc.Text()) {
return // defer 在这里生效,文件被关闭
}
}
}
}
defer f.Close() 写在迭代器函数体里,而不是外面。因为 yield 返回 false 时你会 return,defer 保证文件一定被关。这是手写迭代器最容易漏的地方。
另一个坑:yield 的返回值必须尊重
新手常写成产出后不检查 yield 返回值:
go
// 错误:忽略 yield 返回值,调用方 break 了你还在硬产出
for _, v := range data {
yield(v) // 没判断返回值
}
调用方 break 后再调用 yield,运行时会 panic(range function continued iteration after function returned)。永远写成 if !yield(v) { return }。
小结
- Go 1.23 的 range-over-func 让自定义类型直接支持
for range,底层是iter.Seq[V] = func(yield func(V) bool)。 - 核心优势:惰性求值、零中间拷贝 ,
break会让yield返回false,迭代器立刻停。 - 迭代器可组合:
Filter、Map串成流水线,全程不生成中间 slice;配合slices.Collect/maps.Keys更顺手。 - 两条铁律:持有资源就在迭代器函数体里
defer Close();产出永远写if !yield(v) { return },否则 panic。 - 记忆点:yield 返回 false 就是调用方喊停的信号,尊重它,你的迭代器就既高效又安全。