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

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

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

相关推荐
语戚1 小时前
力扣 3161. 块放置查询:线段树解法(Java 实现)
java·算法·leetcode·面试·线段树·力扣·
CS创新实验室2 小时前
从顺序表到动态数组:数据结构的永恒基石与现代语言的优雅封装
数据结构·算法
Black蜡笔小新2 小时前
自动化AI算法训练服务器DLTM训推一体化平台助力农业生产管理实现安全智能化
人工智能·算法·自动化
8Qi83 小时前
LeetCode 23. 合并 K 个升序链表 —— 小顶堆(PriorityQueue)
数据结构·算法·leetcode·链表·
QiLinkOS4 小时前
《打破“用爱发电”:一种基于 Gitee 与时间戳的开源权益分配机制探索》
c语言·数据结构·c++·科技·算法·gitee·开源
松间听晚4 小时前
Agentic RL 环境和代码学习:以HGPO为例
算法
智者知已应修善业4 小时前
【51单片机用T0定时器方式1,实现0.5S的时间间隔实现第一次一个灯亮、第二次二个灯亮,直到全部灯亮,然后重复整个过程】2023-12-29
c++·经验分享·笔记·算法·51单片机
小许同学记录成长4 小时前
几何体编辑与布尔运算
算法·无人机
fanged5 小时前
简单看看3A算法2(TODO)
算法