Go语言没有传统迭代器接口,95%场景用for range即可;剩余5%优先选func() (T, bool)闭包或Go 1.23的iter.Seq[T],避免自定义Next()/HasNext()结构体——因其不支持range、状态不可重置、并发不安全且易引发panic。

Next() + HasNext() 结构体——95% 的场景,用 for range 就够了;剩下 5%,优先选 func() (T, bool) 闭包,或 Go 1.23 的 iter.Seq[T],别碰自定义接口。
为什么不能直接 return struct{...} + Next() 方法
常见错误是定义一个 type Iterator struct { items []int; idx int },再实现 Next() 和 Done()。这会导致:
-
for v := range it报错:cannot range over it (type Iterator)—— Go 不支持对任意结构体启用range - 多次调用
it.Next()后状态不可重置,也无法并发安全(除非加sync.Mutex,但违背 Go 哲学) - 调用方容易漏判
Done()就直接取值,触发 panic 或返回零值 - 泛型推导失败时,编译器报错晦涩,比如
cannot use ... as iter.Seq[T] value实际只是返回类型写成了func() iter.Seq[T]
func() (T, bool) 是最轻量、最常用的手写方式
它本质是一个带捕获状态的闭包,不暴露字段,无需方法,内存开销小。适用于一次性遍历、分页拉取、树遍历等需保持游标位置的场景。
- 初始化游标为
-1或0,Next()内部递增并判断边界 - 必须在
if ok为 true 后才使用v,否则可能拿到上一轮残留值 - 无法重置:想重新遍历就得再调一次工厂函数,如
iter := IntSliceIter(data) - 注意:不要在循环里对
v取地址传给 goroutine,应显式拷贝:val := v; go handle(&val)
func IntSliceIter(slice []int) func() (int, bool) {
i := -1
return func() (int, bool) {
i++
if i >= len(slice) {
return 0, false
}
return slice[i], true
}
}
// 使用
iter := IntSliceIter([]int{10, 20, 30})
for v, ok := iter(); ok; v, ok = iter() {
fmt.Println(v)
}
Go 1.23 的 iter.Seq[T] 才是唯一能直接 range 的标准方案
这是目前唯一被官方认可、且与 for range 原生兼容的迭代器抽象。签名固定为 func(yield func(T) bool) error,靠 yield 控制消费节奏。
- 返回值必须是
error,不是bool或其他类型,否则range会拒绝 -
yield(v)返回false表示消费者中断(如break),此时必须break循环,否则浪费资源 - 底层若含 IO 或可变数据(如全局切片),需自行处理并发:加锁、复制快照或限制单协程使用
- 不能直接从
[]T转换:必须包装成函数字面量,不能多套一层func() iter.Seq[T]
func SliceSeq[T any](s []T) iter.Seq[T] {
return func(yield func(T) bool) error {
for _, v := range s {
if !yield(v) {
break
}
}
return nil
}
}
// 使用
for v := range SliceSeq([]string{"a", "b", "c"}) {
fmt.Println(v)
}
什么时候该放弃手写,直接用 for range 或回调函数
多数情况下,手写迭代器是过度设计。真正该用原生遍历或函数回调的场景包括:
立即学习“go语言免费学习笔记(深入)”;
- 遍历普通切片、map、channel、string:无条件用
for range,性能最好,语义最清晰 - 需要提前退出、条件过滤、或遍历时修改底层数组:用容器自带的
Range(func(T) bool) bool形式(如sync.Map.Range) - 异步流、IO 密集型遍历(如读文件行、DB 游标):用
chan T,但注意 goroutine 泄露风险和缓冲区大小 - 封装外部数据源(如 HTTP 分页 API):用
iter.Seq[T]+ 闭包捕获 token/offset,比裸 channel 更可控
最容易被忽略的一点:迭代器不是目的,延迟计算、状态封装、统一消费协议才是。如果只是为了“看起来像 Java”,那大概率写错了。


















