Go语言:range over func的实际应用

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

标准库 iterslices 包已经提供了一些组合器,我们也可以自己写:

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)
        }
    }
}

配合 FilterTake,可以很自然地写出「读前 100 行非空行」这类逻辑。

性能考虑

range over func 的调用开销非常小——编译器会把 yield 闭包内联。在 benchmark 中,遍历一个 100 万元素的 iter.Seq[int] 和遍历同等大小的 slice 差距在 5% 以内。不需要担心性能问题。

小结

range over func 让 Go 获得了真正实用的迭代器抽象。它最适合这些场景:惰性求值、封装 I/O 翻页、树/图遍历、组合式数据处理管道。写法上唯一要注意的是始终检查 yield 的返回值并及时 return。