Go语言从1.23版本开始,在标准库中引入了 iter 包,正式确立了迭代器模式。在 iter 包中,迭代器主要分为两种形式:Push(推送)迭代器和 Pull(拉取)迭代器。理解两者的区别与转换是掌握Go新特性的关键。
1. 理解Push迭代器
Push迭代器是Go语言中最基础的迭代器形式,也被称作 Seq(Sequence)。它的本质是一个函数,接收一个“yield”回调函数作为参数。迭代器负责推送数据给yield函数。
定义:type Seq[V any] func(yield func(V) bool)
在这种模式下,迭代器本身控制循环的节奏。当你使用 for range 遍历一个 Seq 时,实际上是 range 在不断调用你传入的函数,直到该函数停止返回 true 或执行完毕。
编写一个简单的Push迭代器:
- 定义一个函数,其签名为
func(yield func(int) bool)。 - 在函数内部,开启一个循环(例如
for循环)。 - 在循环体内,调用
yield(值)将数据传出。 - 检查
yield的返回值,如果为false则 中断 循环。
package main
import "fmt"
// ListNumbers 是一个典型的Push迭代器
func ListNumbers(yield func(int) bool) {
for i := 1; i <= 3; i++ {
// 将值推给 yield,如果 yield 返回 false,则停止
if !yield(i) {
return
}
}
}
func main() {
// 使用 for range 遍历 Push 迭代器
for n := range ListNumbers {
fmt.Println("收到:", n)
}
}
2. 使用Pull迭代器
Push迭代器虽然简单,但有一个限制:它必须一次性运行完毕,无法在中间暂停并根据外部逻辑随时获取下一个值。当你需要手动控制迭代的进度(例如:一次取一个,处理一些逻辑,再取下一个)时,就需要使用 Pull迭代器。
Pull迭代器通过 iter.Pull 函数将一个 Push迭代器转换为一对函数:
next():用于手动获取下一个值。stop():用于停止迭代并释放资源。
转换与使用步骤:
- 准备一个现有的Push迭代器(如上一节的
ListNumbers)。 - 调用
iter.Pull(ListNumbers),获取next, stop两个函数。 - 执行
next()获取值和状态。它返回value, ok。ok为true:表示获取到了有效值。ok为false:表示迭代结束。
- 根据业务逻辑判断是否 继续调用
next()。 - 必须调用
stop()来结束迭代(即使在迭代结束后也建议调用,以确保清理资源)。
package main
import (
"fmt"
"iter"
)
func ListNumbers(yield func(int) bool) {
for i := 1; i <= 3; i++ {
if !yield(i) {
return
}
}
}
func main() {
// 1. 转换为 Pull 迭代器
next, stop := iter.Pull(ListNumbers)
// 4. 确保最终停止迭代器
defer stop()
fmt.Println("准备开始拉取数据...")
// 2. 手动调用 next 获取数据
for {
val, ok := next()
if !ok {
// 3. 如果 ok 为 false,说明没有数据了
break
}
fmt.Printf("手动拉取到: %d\n", val)
// 在这里可以插入任意复杂的逻辑,而不像 range 那样被束缚
if val == 2 {
fmt.Println("拉取到 2,准备结束")
break
}
}
}
3. 处理键值对
除了单值序列 Seq[V],Go还提供了双值序列 Seq2[K, V],常用于遍历Map或同时返回索引和值的场景。对应的Pull转换函数是 iter.Pull2。
使用 iter.Pull2 的逻辑与 iter.Pull 几乎一致:
- 定义或获取一个
Seq2类型的迭代器(例如遍历Map)。 - 调用
iter.Pull2(seq2)获取next和stop。 - 此时的
next函数签名为func() (K, V, bool)。 - 循环调用
next(),解包获取key, value, ok。
package main
import (
"fmt"
"iter"
)
// Pairs 返回一个包含键值对的迭代器
func Pairs(yield func(string, int) bool) {
yield("Apple", 10)
yield("Banana", 20)
yield("Cherry", 30)
}
func main() {
// 转换键值对迭代器
next, stop := iter.Pull2(Pairs)
defer stop()
for {
k, v, ok := next()
if !ok {
break
}
fmt.Printf("Key: %s, Value: %d\n", k, v)
}
}
4. 迭代器模式对比与选择
在开发过程中,如何选择这两种迭代器取决于你对流程控制权的需求。下表总结了它们的区别和使用场景。
| 特性 | Push 迭代器 (Seq) | Pull 迭代器 |
|---|---|---|
| 控制权 | 迭代器内部控制循环,外部被动接收 | 外部调用者控制何时获取下一个值 |
| 语法形式 | func(yield func(V) bool) |
next() (V, bool), stop() |
| 主要用途 | 标准遍历,for range 循环 |
需要手动步进、复杂逻辑判断或异步处理 |
| 转换方式 | 原生形式,无需转换 | 通过 iter.Pull / iter.Pull2 转换 |
| 资源释放 | 通常随循环结束自动结束 | 必须显式调用 stop() 函数 |
核心建议:
- 如果只是简单的遍历数据集,直接使用
for range配合 Push迭代器 即可,这是最简洁的方式。 - 如果需要在遍历过程中暂停、等待网络请求、或者根据复杂的非局部条件决定是否继续获取下一个元素,请使用
iter.Pull转换为 Pull迭代器。

暂无评论,快来抢沙发吧!