Go 迭代器

Go 迭代器

Go 1.23 引入了一个重要的语言特性:for range 现在支持遍历函数 (range over func)。这意味着我们可以为自定义类型实现迭代器,像遍历切片、map 一样使用 for range

什么是 range over func?

简单说,你可以编写一个生成器函数 ,它接收一个回调(通常命名为 yield),然后在内部将元素逐个传给 yield。之后,这个函数就可以直接放在 for range 后面使用。

复制代码
// 一个简单的整数序列迭代器
func Count(n int) func(yield func(int) bool) {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}
​
func main() {
    for v := range Count(5) {
        fmt.Println(v)
    }
}
// 输出 0 1 2 3 4

yield 返回 bool:返回 true 表示继续迭代,返回 false 表示提前终止(对应循环中的 break)。

两类迭代器:Seq 和 Seq2

标准库 iter 定义了两种迭代器类型:

  • iter.Seq[V any]:产生单个值的序列 → for v := range seq

  • iter.Seq2[K, V any]:产生键值对的序列 → for k, v := range seq2

它们的底层都是函数类型:

复制代码
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

推送式迭代器(Pushing Iterator)

推送式迭代器是主动 将元素"推"给回调函数。上面的 Count 就是推送式。Go 的 for range 循环体会被编译器转换成 yield 回调。

例如:

复制代码
for v := range Count(5) {
    fmt.Println(v)
}
// 等价于
Count(5)(func(v int) bool {
    fmt.Println(v)
    return true
})

拉取式迭代器(Pulling Iterator)

拉取式迭代器相反:由调用者 主动"拉"取下一个元素。标准库 iter.Pulliter.Pull2 可以将推送式迭代器转换为拉取式:

复制代码
next, stop := iter.Pull(Count(5))
defer stop()
for {
    v, ok := next()
    if !ok {
        break
    }
    fmt.Println(v)
}

next() 返回元素和一个布尔值表示是否还有值。stop() 必须调用以释放资源(尤其在提前退出时)。

性能上,拉取式比推送式慢很多(见下文),一般优先使用推送式。

错误处理

如果需要处理错误,可以将 error 作为 yield 的第二个参数:

复制代码
func ReadLines(r io.Reader) iter.Seq2[string, error] {
    scanner := bufio.NewScanner(r)
    return func(yield func(string, error) bool) {
        for scanner.Scan() {
            if !yield(scanner.Text(), scanner.Err()) {
                return
            }
        }
    }
}
​
func main() {
    for line, err := range ReadLines(file) {
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(line)
    }
}

标准库中的迭代器

Go 1.23 的 slicesmaps 包新增了许多迭代器工具:

  • slices.All(s) → 索引+值的 Seq2

  • slices.Values(s) → 只返回值的 Seq

  • slices.Chunk(s, n) → 按块分割

  • slices.Collect(seq) → 将迭代器收集为切片

  • maps.Keys(m)maps.Values(m)maps.All(m)

  • maps.Collect(seq2) → 收集为 map

示例:

复制代码
s := []int{1,2,3,4,5}
for chunk := range slices.Chunk(s, 2) {
    fmt.Println(chunk) // [1 2] [3 4] [5]
}
sum := 0
for v := range slices.Values(s) {
    sum += v
}

链式调用

Go 的迭代器本身不支持方法链(因为返回的是函数,不是对象)。但可以自己封装一个小结构体来实现类似效果:

复制代码
type SeqWrapper[E any] struct { seq iter.Seq[E] }
​
func (w SeqWrapper[E]) Map(f func(E) E) SeqWrapper[E] {
    return SeqWrapper[E]{seq: func(yield func(E) bool) {
        for v := range w.seq {
            if !yield(f(v)) {
                return
            }
        }
    }}
}
​
func (w SeqWrapper[E]) Collect() []E {
    return slices.Collect(w.seq)
}
​
// 使用
result := SeqWrapper[int]{seq: slices.Values([]int{1,2,3})}.
    Map(func(x int) int { return x * 2 }).
    Collect()
fmt.Println(result) // [2 4 6]

小结

  • Go 1.23 的 range over func 让自定义迭代器成为可能。

  • 核心是编写一个接受 yield 回调的函数,用 for range 调用它。

  • 标准库 slices/maps 已经广泛支持迭代器,方便数据流处理。

  • 推送式性能好,拉取式慢但有时必要。

  • 这个特性虽然增加了少许复杂度,但大大提升了代码的表达力,值得尝试。

相关推荐
鱼鱼不愚与4 小时前
《原来如此 | 第01期:为什么导航软件能预测红绿灯倒计时?》
算法
复杂网络9 小时前
论最小 Agent 计算机的形态
算法
kisshyshy1 天前
🍦 雪糕、食堂、火车厢:三幅漫画吃透栈、队列与链表
javascript·算法
猿人谷1 天前
不只是 CPU 阈值:STAR 如何用 GAT + Transformer 做容器级自动扩缩容?
人工智能·算法
复杂网络1 天前
Stable Diffusion 视觉大模型微调技术深度调研
算法
复杂网络1 天前
基于 Stable Diffusion 架构的视觉大模型代表性工作与原理深度解析
算法
MrZhao4001 天前
Agent Loop 如何用 Hook 扩展:权限、日志与工具拦截
算法
MrZhao4001 天前
Agent 为什么需要 Skills:别把所有知识都塞进 system prompt
算法
JieE2123 天前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
JieE2124 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试