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.Pull 和 iter.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 的 slices 和 maps 包新增了许多迭代器工具:

  • 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 已经广泛支持迭代器,方便数据流处理。

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

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

相关推荐
INGNIGHT35 分钟前
91 · 最小调整代价(序列dp)
数据结构·算法·leetcode
moshuying35 分钟前
扩散模型开始写代码、开网页了:350 亿参数,速度比 Jev 快一倍
前端·算法
小超人诞生日记42 分钟前
C++ 异常机制详解:从抛出捕获到异常安全
c++·算法
labuladong2 小时前
机器学习&神经网络 系列教程上线
神经网络·算法·机器学习
xiangyun612 小时前
C++ 手撕 shared_ptr:引用计数与控制块到底长什么样?
开发语言·c++·算法·面试·stl
学习论之费曼学习法2 小时前
无人值守AI内容矩阵实战(8):标题 A/B 实验的 6 个工程实现细节,隔日对比与自动判胜
人工智能·python·算法·矩阵·自动化·a/b测试·内容矩阵
Carl_奕然2 小时前
【机器视觉】最强算法之:3D高斯泼溅(3D Gaussian Splatting)
人工智能·python·算法·目标检测·计算机视觉·3d
w_zero_one2 小时前
算法--桶思想
java·开发语言·算法
wordbaby3 小时前
同一次请求里的两道锁:图解搞懂国密 SM2 与 SM3
算法
miller-tsunami4 小时前
内存管理&模板
c++·算法·内存管理·模板