Go中无内置迭代器接口,需手动定义结构体或闭包模拟Next/Value/Done行为;闭包方式轻量但不可重置,自定义集合宜用方法封装;仅当需保持状态或脱离range线性模型时才应手写。

Go 里没有内置迭代器接口,得自己定义 Iterator 类型
Go 语言不提供类似 Java 的 Iterator 接口或 Python 的 __iter__ 协议,所以“手写迭代器”本质是定义一个结构体 + 方法组合,模拟“可移动、可取值、可判断结束”的行为。关键不是继承某个接口,而是约定一组方法名和语义,比如 Next()、Value()、Done()(或 HasNext())。
常见错误是直接返回 []T 或 chan T 就自称“迭代器”——前者是一次性拷贝,后者无法回退、不可重复遍历,也不支持状态控制(如跳过、重置)。真正手写的迭代器应封装状态(当前索引、游标、缓存等)。
-
Next()负责推进内部状态,返回是否还有下一个元素 -
Value()返回当前元素(应在Next()为true后调用) - 避免暴露内部字段;用私有字段 + 公开方法控制访问边界
- 若需并发安全,得加
sync.Mutex,但会降低性能;多数场景下迭代器是单次、单协程使用的
用闭包实现轻量级迭代器比结构体更简洁
对简单集合(如切片、map),用闭包封装游标变量,返回函数类型 func() (T, bool) 是最常用且够用的方式。它天然闭包捕获状态,无需定义结构体、方法、指针接收者,代码更短、内存更轻。
例如遍历 []int:
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{1,2,3})
for v, ok := iter(); ok; v, ok = iter() {
fmt.Println(v)
}
- 闭包迭代器不能重置或倒退;每次调用返回新闭包才能重新开始
- 无法区分“空集合”和“已遍历完”,因为都返回
(zero, false);如有需要,可额外返回 error 或拆成Next()+Value() - 适合只读、一次性、逻辑简单的遍历;不适合需多次
Peek()、Reset()的复杂场景
自定义集合类型内嵌迭代器方法更符合 Go 的组合哲学
如果你在封装自己的集合(如 LinkedList、Tree、RingBuffer),推荐把迭代器作为该类型的**方法**,而非独立函数。这样使用者不用关心底层数据结构,直接调用 collection.Iterator() 即可。
示例(链表节点):
type Node struct {
Val int
Next *Node
}
func (n *Node) Iterator() func() (int, bool) {
curr := n
return func() (int, bool) {
if curr == nil {
return 0, false
}
val := curr.Val
curr = curr.Next
return val, true
}
}
- 方法接收者用指针还是值,取决于是否需修改内部状态(如树的中序遍历可能需维护栈,就得指针)
- 不要让
Iterator()方法返回chan—— 它会启动 goroutine,调用方无法控制生命周期,容易泄漏 - 如果集合支持多种遍历顺序(前序/中序/后序),可提供多个方法:
PreOrderIter()、InOrderIter()等
注意 range 和手写迭代器的适用边界
绝大多数 Go 场景下,直接用 range 就够了:它编译期优化好、语义清晰、无额外分配。手写迭代器只在以下情况必要:
- 需要中途退出并保留位置(比如解析协议流,读到某字段就停,下次从断点继续)
- 底层数据不可随机访问(如 IO reader、数据库 cursor、生成器式计算)
- 需支持外部控制步进节奏(如分页器、调试器单步执行)
- 封装第三方 C 库或硬件驱动,其遍历必须通过特定 API 调用推进
误用典型:为普通切片写一个带 Next()/Value() 的结构体迭代器,性能反而比 range 差,还增加理解成本。Go 的哲学是“少即是多”,手写迭代器是补丁,不是默认方案。
真正难的不是写出来,而是判断“此刻到底该不该手写”——状态是否必须跨调用保持?是否需脱离 for-range 的线性模型?这两个问题的答案为真,才值得动笔。










