Go 1.23 正式引入了 range over func,让自定义迭代器成为一等公民。这篇文章跳过语法介绍,直接看几个实际场景中的迭代器模式。
快速回顾
range over func 允许 for range 遍历一个函数。迭代器函数的签名有三种:
func(yield func() bool) // 无值迭代
func(yield func(V) bool) // 单值迭代(iter.Seq[V])
func(yield func(K, V) bool) // 键值迭代(iter.Seq2[K, V])
yield 返回 false 表示调用方 break 了,迭代器应当停止。
场景一:树的中序遍历
二叉搜索树的中序遍历是最经典的迭代器用例。以前要么递归收集到 slice,要么用 channel,现在可以写成惰性迭代器:
type Node[T cmp.Ordered] struct {
Value T
Left, Right *Node[T]
}
func (n *Node[T]) InOrder() iter.Seq[T] {
return func(yield func(T) bool) {
if n == nil {
return
}
for v := range n.Left.InOrder() {
if !yield(v) {
return
}
}
if !yield(n.Value) {
return
}
for v := range n.Right.InOrder() {
if !yield(v) {
return
}
}
}
}
使用:
for v := range root.InOrder() {
if v > 100 {
break // 提前终止,不会遍历整棵树
}
fmt.Println(v)
}
相比 channel 方案,没有 goroutine 泄漏风险,也不需要 context.Cancel。
场景二:分页 API 遍历
调用分页接口时,把翻页逻辑封装成迭代器,调用方完全不用关心分页细节:
func FetchUsers(client *http.Client, baseURL string) iter.Seq2[User, error] {
return func(yield func(User, error) bool) {
cursor := ""
for {
url := baseURL + "/api/users?limit=100"
if cursor != "" {
url += "&cursor=" + cursor
}
resp, err := client.Get(url)
if err != nil {
yield(User{}, err)
return
}
var page struct {
Users []User `json:"users"`
NextCursor string `json:"next_cursor"`
}
json.NewDecoder(resp.Body).Decode(&page)
resp.Body.Close()
for _, u := range page.Users {
if !yield(u, nil) {
return
}
}
if page.NextCursor == "" {
return
}
cursor = page.NextCursor
}
}
}
调用方:
for user, err := range FetchUsers(client, "https://api.example.com") {
if err != nil {
log.Fatal(err)
}
processUser(user)
}
分页、请求、解码全部隐藏在迭代器内部。如果调用方 break,迭代器直接停止,不会发多余请求。
场景三:组合迭代器——Filter / Map / Take
标准库 iter 和 slices 包已经提供了一些组合器,我们也可以自己写:
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) {
if !yield(v) {
return
}
}
}
}
}
func Map[V, U any](seq iter.Seq[V], f func(V) U) iter.Seq[U] {
return func(yield func(U) bool) {
for v := range seq {
if !yield(f(v)) {
return
}
}
}
}
func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
return func(yield func(V) bool) {
i := 0
for v := range seq {
if i >= n {
return
}
if !yield(v) {
return
}
i++
}
}
}
组合使用:
names := Map(
Filter(
FetchUsersSeq(client, baseURL),
func(u User) bool { return u.Active },
),
func(u User) string { return u.Name },
)
for name := range Take(names, 10) {
fmt.Println(name)
}
这种函数式风格的链式调用在 Go 里终于不别扭了。
场景四:文件逐行读取
func Lines(r io.Reader) iter.Seq2[string, error] {
return func(yield func(string, error) bool) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
if !yield(scanner.Text(), nil) {
return
}
}
if err := scanner.Err(); err != nil {
yield("", err)
}
}
}
配合 Filter 和 Take,可以很自然地写出「读前 100 行非空行」这类逻辑。
性能考虑
range over func 的调用开销非常小——编译器会把 yield 闭包内联。在 benchmark 中,遍历一个 100 万元素的 iter.Seq[int] 和遍历同等大小的 slice 差距在 5% 以内。不需要担心性能问题。
小结
range over func 让 Go 获得了真正实用的迭代器抽象。它最适合这些场景:惰性求值、封装 I/O 翻页、树/图遍历、组合式数据处理管道。写法上唯一要注意的是始终检查 yield 的返回值并及时 return。