供稿来自:@李天鸣
一、背景
Go 1.23 中增加了一个语法特性「支持用户自定义迭代器」同时在标准库中增加了「iter 包」。社区中对于这个特性出现的不同声音:
- 支持方:统一了 Go 混乱的迭代器实现,有利于长期的项目迭代,以及更加友好的用户自定义功能。
- 反对方:增加语言的复杂性,破坏 Go 的显示哲学,认为 Go 正朝着错误的方向发展。
二、使用
iter 包中有两个类型的迭代器:「单值」和「键值对」。
另外,在 maps 和 slices 包中也预置了一些迭代器的方法:
通过案例看代码:将 []int 中的元素进行平方 -> 保留偶数 -> 遍历结果
对于这段代码造成阅读障碍的原因,应该会是「yield」这个关键字,这部分稍后在原理节中会简要说明。
三、优雅的使用
既然都引入了迭代器,能不能让代码看起来更简洁、更可阅读一些呢?例如 javascript:
当然, Go 官方并没有提供“免费的午餐”,这里还是需要我们进行简单的封装:
现在让我们重写一下之前的案例:
四、原理浅析
以 slices 包中的 All 函数为例:
从函数的申明可以看出,其作用是创建一个 iter.Seq2[int, E] 的迭代器。
这里光看代码片段是很难理解的,因为 slices.All 函数返回的是一个另一个函数“func(yield func(K, V) bool)”,那么怎么会返回元素的键值对呢?
没错,糖来了。「for range + 迭代器」就是 Go 提供的语法糖,也就是说我们所看见的代码,实际上在编译阶段会进行代码转换:
所以 for range body 中的逻辑被转换为迭代器的 「yield」函数的实现,当 yield 返回值为 true 时,则会继续进行遍历,直到切片的最后一个元素。
Push 迭代器 与 Pull 迭代器
push 与 pull 是迭代器的两种实现模式。以遍历 slice 为例,一种是主动将每个元素推给 yeild 函数,称为 push;另外一种是,遍历过程中会返回一个函数,调用该函数时它会返回一个元素,另外还需要一个 stop 函数来控制何时停止,称为 pull。
前面我们所见的都是 push 迭代器,Go 官方之称为「标准迭代器」,可以与 for range 语句一起使用。而 pull 迭代器则会需要两个函数「next」与「stop」,好在官方已经提供了转换函数供我们使用,分别对应了 Seq 与 Seq2:
使用示例:
简而言之,在 push 模式不满足情况时,可以考虑采用 pull 模式来实现更为定制化的需求。
关于性能
因为迭代器通过代码转换之后会额外地引入函数调用,官方的说法是这种转换带来的开销是可以通过内联来优化掉的,不过实测下来还是有性能消耗。
总结
本文介绍了 Go 1.23 引入的 iter 迭代器,然后通过案例展示了基础的使用以及函数式的方式,最后简述了实现原理与两种迭代模式。从代码的可读性来看,迭代器增加了代码的理解成本。不过从“对象”的角度来分析,迭代器其实有助于代码封装维护,因为它将分散的逻辑内聚到一起,并且提供了统一的消费模式。最后,你觉得这个颗糖好吃吗?