编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

    • 入门教程

    • 综合案例

    • 专栏博客

      • Go 专栏博客
      • 内存模型与栈堆布局
      • 指针与逃逸分析
      • 结构体内存布局对齐
      • 字符串与切片底层
      • 接口与类型系统
      • map哈希表底层实现
      • 零值初始化设计哲学
      • GMP协程调度器机制
      • 通道channel源码剖析
      • sync同步原语剖析
      • map并发安全与哈希
      • Go内存模型一致性
      • 加权信号量与限流
      • errgroup并行控制
      • 协程泄漏排查与修复
      • 并发设计模式详解
      • GC三色标记与屏障
      • 内存分配器深挖
      • defer延迟执行机制
      • 定时器四叉堆实现
      • 抢占式调度器原理
      • 协程栈扩容与缩容
      • 上下文取消与传播
      • 泛型与类型约束
      • 反射机制与unsafe
      • 迭代器与rangefunc
        • 1. 案例引入
          • 1.1 一段崩在哪
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 推模式 vs 拉模式全景
          • 2.2 为什么走 push 而非 pull 路线
        • 3. iter.Seq 与 Seq2 协议
          • 3.1 Seq[T] 的底层签名
          • 3.2 Seq2[K,V] 的双值迭代
          • 3.3 yield 返回值的控制语义
        • 4. 编译器改写 for-range-over-func
          • 4.1 基本改写模型
          • 4.2 break 如何中止迭代
          • 4.3 defer 与 panic 安全
        • 5. iter.Pull 推转拉机制
          • 5.1 goroutine + channel 的协程式实现
          • 5.2 stop 必须调用的泄漏根源
        • 6. 标准库的迭代器化
          • 6.1 slices 包的迭代器适配
          • 6.2 maps 包的迭代器适配
        • 7. 与其他语言迭代器对比
          • 7.1 Python _iter_ vs Go Seq
          • 7.2 Rust Iterator vs Go Seq
          • 7.3 JS Generator vs Go Pull
        • 8. 可组合性与性能
          • 8.1 组合子函数的开销
          • 8.2 链式迭代器的 defer 栈
        • 9. 诊断与陷阱
          • 9.1 pprof 定位 Pull goroutine 泄漏
          • 9.2 常见陷阱 Top 5
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次 range-over-func 的完整路径
          • 10.3 设计哲学回扣
          • 10.4 速查表
      • 错误处理与panic
      • 网络轮询器netpoller
      • HTTP服务端源码分析
      • JSON序列化与编解码
      • 数据库SQL连接池
      • 文件IO与零拷贝
      • 结构化日志与配置
      • 单元测试与基准
      • cgo与系统调用切换
      • 编译链接与PGO优化
      • 写作模板
    • 开发技巧

  • JavaScript入门

  • CodeX
  • Go入门到精通
  • 专栏博客
杨充
2026-05-23
目录

迭代器与rangefunc

# 26.迭代器与rangefunc

卷三第 26 篇——聚焦 Go 1.23 稳定版迭代器(range over func)——这是 Go 自泛型后的第二大语法变革。本篇从 iter.Seq[T] 和 iter.Seq2[K,V] 的协议定义出发,追踪编译器如何把 for x := range f 改写为传 yield 闭包、break 如何中止迭代器、iter.Pull 如何用 goroutine+channel 把推模式转为拉模式,以及 stop 函数的泄漏陷阱。关键词:iter.Seq、iter.Seq2、yield 函数、编译器改写、iter.Pull、协程式转换、与 Python/Rust/JS 迭代器对比。

# 目录介绍

  • 1. 案例引入
    • 1.1 一段崩在哪
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 推模式 vs 拉模式全景
    • 2.2 为什么走 push 而非 pull 路线
  • 3. iter.Seq 与 Seq2 协议
    • 3.1 Seq[T] 的底层签名
    • 3.2 Seq2[K,V] 的双值迭代
    • 3.3 yield 返回值的控制语义
  • 4. 编译器改写 for-range-over-func
    • 4.1 基本改写模型
    • 4.2 break 如何中止迭代
    • 4.3 defer 与 panic 安全
  • 5. iter.Pull 推转拉机制
    • 5.1 goroutine + channel 的协程式实现
    • 5.2 stop 必须调用的泄漏根源
  • 6. 标准库的迭代器化
    • 6.1 slices 包的迭代器适配
    • 6.2 maps 包的迭代器适配
  • 7. 与其他语言迭代器对比
    • 7.1 Python iter vs Go Seq
    • 7.2 Rust Iterator vs Go Seq
    • 7.3 JS Generator vs Go Pull
  • 8. 可组合性与性能
    • 8.1 组合子函数的开销
    • 8.2 链式迭代器的 defer 栈
  • 9. 诊断与陷阱
    • 9.1 pprof 定位 Pull goroutine 泄漏
    • 9.2 常见陷阱 Top 5
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次 range-over-func 的完整路径
    • 10.3 设计哲学回扣
    • 10.4 速查表

# 1. 案例引入

# 1.1 一段崩在哪

某数据平台的 map/reduce 聚合引擎在 Go 1.22 → 1.23 升级后全面引入 range over func 迭代器。他们为自定义数据结构 OrderedMap[K,V] 实现了 All() 方法,并用 iter.Pull2 在兼容旧代码处将推模式转为拉模式。上线两周后,goroutine 数从 200 稳定涨到 12000,RSS 持续增长,最终 OOM Kill。

// ordered_map.go —— 有序 map 迭代器版
package main

import (
    "iter"
    "sort"
)

type OrderedMap[K comparable, V any] struct {
    data  map[K]V
    order []K
}

// All 返回推模式迭代器(Go 1.23 标准方式)
func (m *OrderedMap[K, V]) All() iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        for _, k := range m.order {
            if !yield(k, m.data[k]) {
                return  // 消费者要求停止
            }
        }
    }
}

// TopN 返回前 N 个条目——兼容旧代码用 Pull 拉模式
func (m *OrderedMap[K, V]) TopN(n int) []struct{K; V} {
    next, stop := iter.Pull2(m.All())  // ← 创建 goroutine
    //                                       ↑ stop 在下面被遗忘了
    var result []struct{K; V}
    for i := 0; i < n; i++ {
        k, v, ok := next()
        if !ok {
            break
        }
        result = append(result, struct{K; V}{k, v})
    }
    // ❌ 没有调用 stop()!如果 n < len(m.order),goroutine 永远泄漏
    return result
}

// 业务侧调用——每秒数千次
func handleQuery(m *OrderedMap[string, float64]) {
    // 只需要 Top 10——但 map 有 10000 个条目
    top10 := m.TopN(10)  // 每次调用泄漏一个 goroutine!
    // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

现象:

  • OrderedMap 有 10000 条记录,TopN(10) 只取前 10 条
  • iter.Pull2 内部启动 goroutine 执行 All() 的闭包
  • All() 遍历 10000 条,但前 10 条后 yield 返回 false(因为 TopN 循环只取 10 次)
  • All() 闭包检测到 yield 返回 false → return → goroutine 退出
  • 但实际上没有——TopN 只取了 10 次就退出 for 循环,但迭代器 goroutine 中的 All() 闭包被阻塞在 yield 调用上——yield 底层是向 channel 发送数据,消费者已经不再接收,发送永久阻塞

# 1.2 顺藤摸到根因

追查:

  • 假设 1:是不是 All() 的闭包不会自动退出?—— 看源码:All() 在 yield 返回 false 时 return。但如果消费者在 yield 之前就停止拉取呢?发送方阻塞在 channel send 上。

  • 假设 2:iter.Pull2 的实现是什么?—— 看 iter/iter.go(Go 1.23):

// iter/iter.go (概念模型——展示核心机制)
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) {
    coro := newcoro()        // 创建协程状态
    ch := make(chan struct{ K; V; bool })  // 通信通道

    go func() {
        seq(func(k K, v V) bool {
            ch <- struct{ K; V; bool }{k, v, true}  // yield → send
            return <-coro.resume                    // 等待消费者恢复
        })
        close(ch)
    }()

    next = func() (K, V, bool) {
        val, ok := <-ch     // 拉取 → receive
        if !ok { return }
        coro.resume <- true // 通知迭代器继续
        return val.K, val.V, val.ok
    }

    stop = func() {
        // 通知 goroutine 退出——细节省略
    }

    return next, stop
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

核心机制:iter.Pull2 用 goroutine + 双向通信通道 实现。迭代器 goroutine 通过 ch 发送每个条目,通过 coro.resume 等待消费者拉取下一个。stop() 负责通知 goroutine 退出并清理通道。

问题所在:TopN 没有调用 stop()——迭代器 goroutine 阻塞在 ch <- ... 上(消费者不再接收了)。这个 goroutine 永远不会退出——因为它是活跃的、非 deadlock 的 goroutine(有 channel 操作在等待)。

  • 假设 3:goroutine 数量能不能通过 pprof 验证?—— 看 pprof:
$ go tool pprof http://localhost:6060/debug/pprof/goroutine
(pprof) top5
      flat  flat%   sum%        cum   cum%
      9800 81.67% 81.67%       9800 81.67%  runtime.chansend
1
2
3
4

9800 个 goroutine 卡在 chansend——正是 yield 内部的 ch <- ... 操作。消费者已停止拉取,发送方永久阻塞。

这个案例藏着至少 7 个原理点:

① iter.Seq[T] 的类型签名是什么?yield 参数的作用是什么?       → 第 3 章
② 编译器如何把 for range f 改写为传递闭包?                  → 第 4 章
③ break 如何通过 yield 返回值中止迭代器?                    → 第 4.2
④ iter.Pull 内部如何用 goroutine+channel 实现推转拉?        → 第 5 章
⑤ 为什么 Pull 的 stop 必须调用?                            → 第 5.2
⑥ slices.All / maps.Keys 等标准库函数怎么封装?              → 第 6 章
⑦ pprof 如何定位 Pull 导致的 goroutine 泄漏?                → 第 9 章
1
2
3
4
5
6
7

# 1.3 我们要回答什么

这个 OrderedMap 案例就是本篇的主线案例。我们从 iter.Seq[T]/iter.Seq2[K,V] 的类型定义出发,追踪编译器对 for range f 的改写机制,然后深入 iter.Pull 的 goroutine+channel 协程式实现和 stop 泄漏陷阱,最后回到聚合引擎,给出"Always call stop + defer 保障"的修复方案。

本篇路线:

协议定义 (第 3 章) ── Seq[T]、Seq2[K,V] 的 yield 签名
   ↓
编译器改写 (第 4 章) ── for-range 如何变闭包传递
   ↓
Pull 机制 (第 5 章) ── goroutine+channel 推转拉
   ↓
标准库 (第 6 章) ── slices/maps 的迭代器适配
   ↓
跨语言对比 (第 7 章) ── Python/Rust/JS 各有什么不同
   ↓
可组合性 (第 8 章) ── 链式调用与 defer 栈
   ↓
诊断实战 (第 9 章) ── pprof + 陷阱 Top 5
   ↓
综合案例 (第 10 章) ── 回到 OrderedMap,根治 goroutine 泄漏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

📌 本篇定位:迭代器是 Go 1.23 的第二个"语法级 feature"(第一个是泛型)。它不是一个新的类型——是编译器对函数类型 func(yield func(T) bool) 的语法糖。读完本篇,我们能回答:"for k, v := range myMap.All() 被编译器翻译成了什么?iter.Pull 里的 goroutine 在什么条件下会泄漏?"

# 2. 架构概览

# 2.1 推模式 vs 拉模式全景

Go 迭代器的核心设计空间是两个方向:

            ┌─────────────────────────────────┐
            │        迭代器两种模式              │
            └─────────────────────────────────┘
                         │
        ┌────────────────┴────────────────┐
        ▼                                 ▼
   推模式 (push)                      拉模式 (pull)
   迭代器"推送"给消费者              消费者"拉取"从迭代器
   ────────────────               ────────────────
   iter.Seq[T]                   iter.Pull(seq)
   iter.Seq2[K,V]                iter.Pull2(seq)
   ────────────────               ────────────────
   原生模式:编译器支持            桥接模式:用于兼容旧代码
   for k, v := range seq         next, stop := Pull(seq)
   零额外 goroutine              启动一个新 goroutine
   直接函数调用                   通过 channel 通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

推模式的本质——迭代器函数接收一个 yield 回调并调用它来"推送"每个元素:

迭代器函数:  func(yield func(T) bool)
                       ↑
消费者代码 ←── for x := range iterator ──→ yield(x)

编译器生成的闭包:  body(x) { 消费者逻辑; return 是否继续 }
1
2
3
4
5

拉模式的本质——把推模式包在一个 goroutine 里,通过 channel 把"推送"变成"轮询":

迭代器 goroutine:  yield(x) → ch <- x → 等待 resume
                          ↑            ↓
消费者 goroutine:      <- ch         resume <- true
1
2
3

# 2.2 为什么走 push 而非 pull 路线

疑惑:Python、Rust、JS 都是 pull 模式的迭代器(next() 主动拉取)——为什么 Go 选择 push 模式作为原生?

论证:

  1. for-range 天生是 push 语义——Go 的 for _, v := range slice 本身就是"运行时把元素推送给你"。push 迭代器是 for-range 的自然扩展——不需要改变 for-range 的语义,只需让 range 后面的表达式可以是一个 push 迭代器函数。

  2. push 不需要 goroutine——原生 push 迭代器就是一次函数调用。for x := range seq 被编译器改写为 seq(func(x T) bool { ... })——没有任何 goroutine、没有 channel、没有调度开销。对比 pull 模式必然需要 goroutine+channel 的桥接。

  3. push 的 break 语义天然正确——break 时,编译器生成代码让 yield 回调返回 false。迭代器函数检查 if !yield(x) { return } 即可立即停止迭代和清理资源(defer 自动运行)。pull 模式中,如果消费者不再调用 next()——迭代器需要额外的 stop() 机制来感知退出。

  4. push 与 Go 的 channel 哲学一致——Go 中 for msg := range ch 是 push 模式(channel 把消息推送给消费者)。迭代器的 push 语义与此一脉相承。

结论:push 是 Go 的天然选择——它不需要 runtime 改动(纯编译器语法糖)、零额外 goroutine、资源清理由 defer 自动处理。pull 是 push 的包装——仅在需要"手动控制拉取节奏"时使用,并承担 goroutine 泄漏的风险。

# 3. iter.Seq 与 Seq2 协议

# 3.1 Seq[T] 的底层签名

iter.Seq[T] 的定义异常简洁(iter/iter.go):

// iter/iter.go (Go 1.23+)
package iter

type Seq[T any] = iter.Seq[T]  // 标准库中 iter 包对外暴露的类型定义
// 实际等价于:
// type Seq[T any] func(yield func(T) bool)
1
2
3
4
5
6

func(yield func(T) bool) 的含义:

  • 迭代器函数接收一个 yield 回调
  • 每次"产出"一个值时,调用 yield(value)
  • yield 返回 bool:true = 消费者还要更多,false = 消费者要求停止

最小的 Seq 实现:

// 一个产出三个整数的迭代器
func ThreeInts() iter.Seq[int] {
    return func(yield func(int) bool) {
        if !yield(1) { return }
        if !yield(2) { return }
        if !yield(3) { return }
    }
}

// 使用
for v := range ThreeInts() {
    fmt.Println(v)  // 1, 2, 3
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3.2 Seq2[K,V] 的双值迭代

iter.Seq2[K, V] 用于需要键值对的场景——map、有序集合等:

type Seq2[K, V any] func(yield func(K, V) bool)
//                                     ↑      ↑
//                                   键     值
1
2
3

Seq2 的典型用法:

// map 的迭代器
func MapEntries[K comparable, V any](m map[K]V) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        for k, v := range m {
            if !yield(k, v) {
                return
            }
        }
    }
}

// 使用——双变量 for-range
for k, v := range MapEntries(myMap) {
    fmt.Println(k, v)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Seq[T] vs Seq2[K,V] 在 for-range 中的映射:

迭代器类型 for-range 语法 变量数量
iter.Seq[T] for v := range seq 1 个
iter.Seq2[K,V] for k, v := range seq 2 个

# 3.3 yield 返回值的控制语义

yield 的返回值是 push 迭代器的流量控制机制:

func CustomSeq() iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < 1000; i++ {
            // 耗时长的计算...
            expensiveComputation(i)

            if !yield(i) {
                // ← 消费者说了"停"
                // 清理资源(defer 自动运行)
                cleanup()
                return  // 立即退出迭代
            }
            // yield 返回 true → 消费者还要更多
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

编译器生成的消费者闭包中 yield 的返回值逻辑:

编译器为 for v := range seq { body } 生成的代码:
  seq(func(v T) bool {
      body          // ← 执行循环体
      return true   // ← 没有 break → 返回 true(继续)
  })

如果有 break:
  seq(func(v T) bool {
      body
      return false  // ← break → 返回 false(停止)
  })
1
2
3
4
5
6
7
8
9
10
11

# 4. 编译器改写 for-range-over-func

# 4.1 基本改写模型

编译器对 for x := range f(其中 f 是 func(yield func(T) bool) 类型)的改写是纯语法糖——不需要 runtime 参与:

源码:                       编译器改写为:
─────────────────────       ───────────────────────────
for x := range f {         f(func(x T) bool {
    body(x)                    body(x)
}                              return true  // 没有 break
                           })
1
2
3
4
5
6

完整示例:

// 源码
sum := 0
for v := range ThreeInts() {
    sum += v
}
fmt.Println(sum)

// 编译器改写后(语义等价)
sum := 0
ThreeInts()(func(v int) bool {
    sum += v
    return true  // 没有 break → 继续
})
fmt.Println(sum)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

关键:消费者闭包引用了外部变量 sum——这个闭包就是 Go 编译器生成的隐式闭包。ThreeInts() 返回的迭代器函数接收这个闭包作为 yield 参数。

# 4.2 break 如何中止迭代

break 通过让 yield 闭包返回 false 来通知迭代器停止:

源码:                       编译器改写为:
─────────────────────       ───────────────────────────
for x := range f {         f(func(x T) bool {
    if cond {                   if cond {
        break                       return false  // ← break!
    }                           }
}                               body(x)  // 没有 break 时才走到
                                return true
                            })
1
2
3
4
5
6
7
8
9

多语句循环体的改写:

// 源码
for v := range seq {
    fmt.Println(v)
    if v > 10 {
        break
    }
    doMore(v)
}

// 编译器改写
seq(func(v int) bool {
    fmt.Println(v)
    if v > 10 {
        return false  // break → 停止迭代
    }
    doMore(v)
    return true  // 正常结束本轮 → 继续
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

continue 不需要特殊处理——因为它就是"结束本轮循环体,继续下一轮"。闭包正常返回 true 即可。

# 4.3 defer 与 panic 安全

迭代器函数中可以安全使用 defer:

func FileLines(path string) iter.Seq[string] {
    return func(yield func(string) bool) {
        f, err := os.Open(path)
        if err != nil {
            return
        }
        defer f.Close()  // ← 无论迭代如何结束,都会关闭文件

        scanner := bufio.NewScanner(f)
        for scanner.Scan() {
            if !yield(scanner.Text()) {
                return  // break → yield 返回 false → 这里 return
                         // defer f.Close() 自动运行
            }
        }
        // 正常耗尽 → defer f.Close() 也运行
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

panic 安全——如果消费者闭包中发生 panic,迭代器函数中的 defer 仍然执行(因为闭包在迭代器函数的调用栈中):

// 即使在迭代中 panic,defer 仍然清理
for line := range FileLines("/tmp/data.txt") {
    if line == "" {
        panic("empty line!")  // → 栈展开 → FileLines 的 defer f.Close() 执行
    }
}
1
2
3
4
5
6

# 5. iter.Pull 推转拉机制

# 5.1 goroutine + channel 的协程式实现

iter.Pull 和 iter.Pull2 是 push 到 pull 的桥接——用一个 goroutine 执行 push 迭代器,通过 channel 把"推送"转为"拉取":

Pull2(seq Seq2[K,V]) 内部工作流程:
──────────────────────────────────────────
消费者 goroutine              迭代器 goroutine(新启动)
───────────────────           ─────────────────────────
                               seq(func(k, v) {
next() ←─ ch ──────               ch <- {k, v, true}
  │                                │
  │ 获取到值                        │ 等待消费者恢复
  │                                ▼
  ├─ resume → ──────────       <- resume (消费者已取走)
  │                                │
  │ 处理取到的值                    │ yield 返回 true → 继续迭代
  │                                │
next() ←─ ch ──────                │ (下一个值)
  ...                              ...

stop() ── 通知 ───────────→    goroutine 退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

核心数据结构(概念模型——实际实现用 runtime.coro 而非原始 goroutine):

func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) {
    // 1. 创建协程状态
    var (
        coro   coroutine      // 协程控制
        val    K, V           // 当前元素
        ok     bool
        done   bool
    )

    // 2. 启动协程执行 push 迭代器
    coro = newCoroutine(func() {
        seq(func(k K, v V) bool {
            val, ok = k, v
            val = k; ok = true
            coro.suspend()   // 挂起——等待消费者取走
            return !done     // 如果 stop 被调用,返回 false
        })
        ok = false           // 迭代结束
        coro.suspend()
    })

    // 3. next 函数——拉取下一个值
    next = func() (K, V, bool) {
        if !ok { return }
        coro.resume()        // 恢复迭代器协程(让它执行 yield 调用)
        // 此时迭代器已通过 yield 给出了下一个值
        return val.K, val.V, ok
    }

    // 4. stop 函数——终止迭代
    stop = func() {
        done = true
        coro.resume()        // 恢复协程让它退出
    }

    return next, stop
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

注意:Go 1.23 的实际实现用了 runtime.coro(基于 Russ Cox 的协程提案),而非原始 goroutine。但语义等价——都是"一个执行上下文通过 channel 与另一个通信"。

# 5.2 stop 必须调用的泄漏根源

stop() 必须被调用的原因:

场景 1:正常耗尽迭代器(ok == false)
  → 协程自然退出 → stop 不需要(但调用也无害)

场景 2:提前停止(只取了前 N 个)
  → next 不再被调用 → 协程阻塞在 yield 中
  → 永不退出 → goroutine 泄漏 + 协程持有的所有资源泄漏
  → 必须调用 stop() 通知协程退出

场景 3:消费者 panic
  → 协程同样阻塞
  → stop 必须在 defer 中调用
1
2
3
4
5
6
7
8
9
10
11

正确的 Pull 使用模式:

// ✅ 正确:defer stop()
func TopN[K comparable, V any](m *OrderedMap[K, V], n int) []V {
    next, stop := iter.Pull2(m.All())
    defer stop()  // ← 无论函数怎么退出,stop 都会被调用

    var result []V
    for i := 0; i < n; i++ {
        _, v, ok := next()
        if !ok { break }
        result = append(result, v)
    }
    return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13

为什么 Go 不自动在 next 返回 ok=false 时调用 stop——设计选择:让调用方显式管理资源。stop() 是幂等的(多次调用安全),推荐总是 defer stop()。

# 6. 标准库的迭代器化

# 6.1 slices 包的迭代器适配

Go 1.23 为标准库 slices 添加了迭代器适配函数(slices 包):

函数 返回类型 产出的值
slices.All(s) iter.Seq2[int, E] (索引, 值)
slices.Values(s) iter.Seq[E] 值
slices.Backward(s) iter.Seq2[int, E] (索引, 值)——从后向前
slices.Collect(seq) []E 从迭代器收集到切片
slices.Sorted(seq) []E 从迭代器收集并排序
// 使用 slices.All 遍历切片的索引和值
s := []string{"a", "b", "c"}
for i, v := range slices.All(s) {
    fmt.Printf("%d: %s\n", i, v)
}
// 输出: 0: a, 1: b, 2: c

// slices.Backward —— 反向迭代
for _, v := range slices.Backward(s) {
    fmt.Println(v)
}
// 输出: c, b, a
1
2
3
4
5
6
7
8
9
10
11
12

# 6.2 maps 包的迭代器适配

函数 返回类型 产出的值
maps.All(m) iter.Seq2[K, V] (键, 值)
maps.Keys(m) iter.Seq[K] 键
maps.Values(m) iter.Seq[V] 值
maps.Collect(seq) map[K, V] 从迭代器收集到 map
m := map[string]int{"a": 1, "b": 2}
for k, v := range maps.All(m) {
    fmt.Printf("%s: %d\n", k, v)
}

// 只迭代键
for k := range maps.Keys(m) {
    fmt.Println(k)
}
1
2
3
4
5
6
7
8
9

# 7. 与其他语言迭代器对比

# 7.1 Python iter vs Go Seq

特性 Python Go
协议 __iter__() 返回迭代器对象 + __next__() 取下一个 func(yield func(T) bool)
模式 拉模式(pull) 推模式(push)
停止信号 raise StopIteration yield 返回 false
资源清理 __del__ 或 with context manager defer(迭代器函数)
goroutine 不涉及 原生 push 模式不涉及;Pull 涉及

Python push → pull 的语义差异:

# Python: 拉模式——消费者控制节奏
it = iter([1, 2, 3])
x = next(it)  # 拉取第 1 个
y = next(it)  # 拉取第 2 个
1
2
3
4
// Go: 推模式——迭代器控制节奏(在原生 push 模式中)
for x := range Seq() {  // Seq() 把值推给消费者
    // ...
}
1
2
3
4

# 7.2 Rust Iterator vs Go Seq

特性 Rust Go
协议 Iterator trait + next() func(yield func(T) bool)
组合子 .map().filter().collect() 手动嵌套 for-range 或链式函数
惰性求值 是(组合子不触发执行) 否——for-range 立即触发
零成本抽象 编译器内联组合子 每层嵌套多一次函数调用

Rust 的迭代器优势——组合子自动生成高效代码:

// Rust: 链式组合子——编译器优化为单循环
let result: Vec<i32> = (0..1000)
    .filter(|x| x % 2 == 0)
    .map(|x| x * 3)
    .collect();
1
2
3
4
5
// Go: 需要手动嵌套——或在循环中直接编写逻辑
result := make([]int, 0)
for x := range seq {
    if x%2 == 0 {
        result = append(result, x*3)
    }
}
1
2
3
4
5
6
7

# 7.3 JS Generator vs Go Pull

JS 的 Generator 函数是最接近 Go iter.Pull 的概念:

// JS Generator——拉模式
function* gen() {
    yield 1;
    yield 2;
    yield 3;
}
const it = gen();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
1
2
3
4
5
6
7
8
9
// Go Pull——拉模式(包装 push)
next, stop := iter.Pull(ThreeInts())
v1, ok1 := next()  // 1, true
v2, ok2 := next()  // 2, true
defer stop()
1
2
3
4
5

核心差异——JS Generator 的语言级支持使它不需要额外 goroutine;Go 的 Pull 是纯库级实现——底层是 goroutine+channel 的协程桥接。

# 8. 可组合性与性能

# 8.1 组合子函数的开销

Go 迭代器可以通过函数组合来实现 map/filter 语义——但每次组合增加一层函数调用:

// Filter 组合子
func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        seq(func(v T) bool {       // 第 1 层闭包
            if pred(v) {
                return yield(v)    // 第 2 层闭包
            }
            return true
        })
    }
}

// Map 组合子
func Map[T, U any](seq iter.Seq[T], fn func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        seq(func(v T) bool {       // 第 1 层闭包
            return yield(fn(v))    // 第 2 层闭包
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

链式调用时每层嵌套一次函数调用——Filter → Map → Filter 的链式调用意味着每产出一个值要经过 3 层 yield 调用。在百万级数据量下,这个开销可能明显。

# 8.2 链式迭代器的 defer 栈

嵌套迭代器中的 defer 执行顺序:

func Outer() iter.Seq[int] {
    return func(yield func(int) bool) {
        defer fmt.Println("outer cleanup")  // ← 最后执行
        for v := range Inner() {            // Inner 的 defer 先执行
            if !yield(v) { return }
        }
    }
}

func Inner() iter.Seq[int] {
    return func(yield func(int) bool) {
        defer fmt.Println("inner cleanup")  // ← 先执行
        yield(1)
        yield(2)
    }
}
// 输出顺序: inner cleanup → outer cleanup
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

defer 保证了资源逐层清理——最内层先清理,最外层最后清理。

# 9. 诊断与陷阱

# 9.1 pprof 定位 Pull goroutine 泄漏

步骤一:goroutine profile

$ go tool pprof http://localhost:6060/debug/pprof/goroutine
(pprof) top10
      flat  flat%   sum%        cum   cum%
      9800 81.67% 81.67%       9800 81.67%  runtime.chansend
       800  6.67% 88.33%        800  6.67%  iter.Pull2
1
2
3
4
5

runtime.chansend 占比 > 50% + iter.Pull2 在调用栈中 → Pull 泄漏。

步骤二:看具体卡在哪个函数

$ curl http://localhost:6060/debug/pprof/goroutine?debug=2 | grep -B5 "chan send"
goroutine 4872 [chan send]:
main.(*OrderedMap[...]).All.func1()
    /app/ordered_map.go:14 +0x8f
    ↑ yield(k, m.data[k]) 卡在发送
1
2
3
4
5

步骤三:堆 profile 看资源泄漏

$ go tool pprof http://localhost:6060/debug/pprof/heap
(pprof) top
      flat  flat%
    180MB 45.00%  iter.coroState   # Pull 创建的协程状态对象堆积
1
2
3
4

# 9.2 常见陷阱 Top 5

陷阱 1:Pull 没调 stop(第 1 章根因)

// ❌ stop 忘了
next, stop := iter.Pull2(seq)
// ...
// 函数返回 → stop 永远不被调用 → 协程泄漏

// ✅ defer stop()
next, stop := iter.Pull2(seq)
defer stop()
1
2
3
4
5
6
7
8

陷阱 2:闭包中修改外部变量导致 data race

sum := 0
for v := range seq {
    go func() {
        sum += v  // ❌ data race!
    }()
}
// 迭代器可能在另一个 goroutine(如果 Pull)中运行
// 编译器生成的闭包也可能引用外部变量
1
2
3
4
5
6
7
8

陷阱 3:在迭代器函数中使用 defer 期望立即执行

// ❌ defer f.Close() 只在迭代器函数返回时执行
// Push 模式:break 时 yield 返回 false → 迭代器函数 return → defer 执行 ✓
// Pull 模式:stop() 调用 → 协程退出 → defer 执行 ✓
// 正常:OK
1
2
3
4

陷阱 4:迭代器函数中启动新 goroutine 导致 defer 时序错乱

// ❌ 危险:goroutine 在迭代器函数 return 后还在运行
func BadSeq() iter.Seq[int] {
    return func(yield func(int) bool) {
        defer cleanup()  // ← cleanup 可能在 goroutine 还在跑时执行
        go func() {
            for i := 0; i < 100; i++ {
                yield(i)
            }
        }()
    }
}
1
2
3
4
5
6
7
8
9
10
11

陷阱 5:Pull 迭代器的 stop 必须在所有 next 调用完毕后调用

// ❌ stop 被调用但 next 还被继续使用
next, stop := iter.Pull2(seq)
k1, _, _ := next()
stop()
k2, _, ok := next()  // ← stop 之后 next 行为未定义
1
2
3
4
5
// ✅ 要么全部取完,要么 defer stop + 提前返回
next, stop := iter.Pull2(seq)
defer stop()
for {
    _, v, ok := next()
    if !ok { break }
    if v > threshold { return }  // stop 在 defer 中自动调用
}
1
2
3
4
5
6
7
8

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章 OrderedMap 聚合引擎的七个疑问,逐条作答:

疑问 答案
① iter.Seq[T] 的类型签名是什么? 第 3.1:func(yield func(T) bool)——接收 yield 回调的推模式迭代器
② 编译器如何改写 for-range-over-func? 第 4.1:for x := range f → f(func(x T) bool { ... })——纯语法糖
③ break 如何中止迭代器? 第 4.2:编译器生成的闭包返回 false → yield 返回 false → 迭代器检查并 return
④ Pull 如何实现推转拉? 第 5.1:goroutine + channel 协程桥接——yield = channel send, next = channel receive
⑤ stop 为什么必须调用? 第 5.2:迭代器 goroutine 阻塞在 channel send 上——永不退出
⑥ slices.All 等标准库怎么封装? 第 6 章:把底层容器的遍历包装为 func(yield ...) 闭包
⑦ pprof 怎么看 Pull 泄漏? 第 9.1:runtime.chansend 占比异常 + goroutine?debug=2 定位

案例根因链条:

OrderedMap.All() 返回 iter.Seq2[K,V]
  → TopN 调用 iter.Pull2(seq) → 创建协程执行 All() 闭包
  → TopN 循环取 N 次后返回
  → stop() 从未被调用
  → 迭代器协程阻塞在 yield 内部的 channel send 上
  → 每次 TopN 调用泄漏一个 goroutine + 协程状态
  → 1000 QPS → 3600 秒/小时 × 泄漏率 = 数千~数万 goroutine
  → goroutine 数 12000 → RSS 持续增长 → OOM Kill
1
2
3
4
5
6
7
8

修复方案——三选一:

// 方案 A:加 defer stop(根治)
func (m *OrderedMap[K, V]) TopN(n int) []struct{K; V} {
    next, stop := iter.Pull2(m.All())
    defer stop()  // ← 加这一行就根治泄漏
    // ...
}

// 方案 B:不用 Pull——直接用 push 迭代器
func (m *OrderedMap[K, V]) TopN(n int) []struct{K; V} {
    result := make([]struct{K; V}, 0, n)
    count := 0
    for k, v := range m.All() {
        if count >= n { break }
        result = append(result, struct{K; V}{k, v})
        count++
    }
    return result  // 零 goroutine,零泄漏风险
}

// 方案 C:用 iter.Seq2 的包装——限制迭代次数
func Take[K, V any](seq iter.Seq2[K, V], n int) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        count := 0
        seq(func(k K, v V) bool {
            if count >= n { return false }
            count++
            return yield(k, v)
        })
    }
}
// 使用:for k, v := range Take(m.All(), 10) { ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 10.2 一次 range-over-func 的完整路径

以 for k, v := range m.All() 为例——OrderedMap 有 10000 条记录:

源码: for k, v := range m.All() { fmt.Println(k, v) }
────────────────────────────────────────────────

编译器改写阶段:
  → 识别 m.All() 返回 iter.Seq2[string, float64]
  → 生成消费者闭包:
     func(k string, v float64) bool {
         fmt.Println(k, v)
         return true  // 没有 break → 一直返回 true
     }
  → 改写为: m.All()(generatedClosure)

运行阶段:
  All() 闭包执行:
    for _, k := range m.order {          // 10000 次迭代
        if !yield(k, m.data[k]) {       // yield = generatedClosure
            return                       // 消费者返回 false 才会走到这里
        }
    }

    // 10000 次 yield 调用 → 10000 次 generatedClosure 调用
    // 无 goroutine 创建
    // 无 channel 通信
    // 纯函数调用 → 每次 ~2-3ns(编译器内联后 ~1ns)

总开销: 10000 × ~2ns ≈ 20μs (不含 fmt.Println)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 10.3 设计哲学回扣

哲学 1:用编译器语法糖代替 runtime 机制

Go 迭代器的原生模式不需要 goroutine、不需要 channel、不需要 vtable——它只是编译器对 func(yield func(T) bool) 的语法糖。这个设计让迭代器的核心路径跟手写 for 循环性能一致。Go 团队始终坚持"运行时增量为零"的原则——泛型、迭代器、都是编译器层面的事。

哲学 2:推模式是 Go 并发哲学的延伸

Go 的 channel 是推模式的——for msg := range ch 中,发送方推消息给接收方。迭代器遵循同一范式——生产者推送值给消费者。这种一致性降低了学习成本:理解 channel 的开发者瞬间理解迭代器。

哲学 3:Pull 是 Push 的降级——权衡明确

iter.Pull 是唯一引入 goroutine 的迭代器路径——它的文档和命名明确表明"这不是默认推荐的用法"。Go 团队没有试图隐藏 Pull 的成本(额外 goroutine + channel + stop 泄漏风险),而是让这些成本显式暴露——调用方看到 Pull 就知道"这里有 goroutine"。

哲学 4:资源清理交给 defer——零魔法

迭代器的资源释放不依赖任何"自动"机制——defer 是 Go 的唯一资源清理原语,迭代器完全信任它。无论是 break(yield 返回 false → 迭代器 return → defer 执行)还是正常耗尽(迭代器循环结束 → defer 执行),defer 的语义都是确定的。没有 Close() 接口、没有 Dispose() 模式——这是 Go 简洁哲学的又一体现。

# 10.4 速查表

迭代器类型速查:

类型 签名 使用场景 for-range 语法
iter.Seq[T] func(yield func(T) bool) 单值序列 for v := range seq
iter.Seq2[K,V] func(yield func(K,V) bool) 键值对序列 for k, v := range seq

Pull vs Push:

特性 Push (Seq) Pull
goroutine 0 1 个
channel 0 2 个(数据+控制)
break 支持 yield 返回 false next 返回 ok=false
资源泄漏风险 无(defer 自动清理) 有(必须调用 stop)
使用场景 for-range 直接遍历 需要手动控制拉取节奏

标准库迭代器函数:

包 函数 返回
slices All(s) Seq2[int, E]
slices Values(s) Seq[E]
slices Backward(s) Seq2[int, E]
slices Collect(seq) []E
maps All(m) Seq2[K, V]
maps Keys(m) Seq[K]
maps Values(m) Seq[V]

诊断命令:

# Pull goroutine 泄漏定位
go tool pprof http://localhost:6060/debug/pprof/goroutine  # 看 runtime.chansend 占比
curl localhost:6060/debug/pprof/goroutine?debug=2 | grep "chan send"

# 协程状态泄漏
go tool pprof http://localhost:6060/debug/pprof/heap  # 看 iter.coroState 堆积

# 迭代器性能分析
go test -bench=. -benchmem -count=5 ./...  # Push vs Pull benchmark

# Go 版本确认
go version  # 需要 Go 1.23+(GOEXPERIMENT=rangefunc 在 1.22 中实验性开启)

# 编译期查看改写结果
go tool compile -S file.go 2>&1 | grep "yield"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

下一篇:我们已经掌握了 Go 迭代器的推拉两种模式——从编译器语法糖到 goroutine 协程桥接。下一步进入 27.语言边界互操作——看看 Go 与 C/WebAssembly 的跨语言调用如何通过 cgo 和 syscall 实现栈切换和性能开销的精确量化。

上次更新: 2026/06/13, 21:14:36
反射机制与unsafe
错误处理与panic

← 反射机制与unsafe 错误处理与panic→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式