Go1.23 糖果 —— iter 迭代器

供稿来自:@李天鸣

一、背景

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 迭代器,然后通过案例展示了基础的使用以及函数式的方式,最后简述了实现原理与两种迭代模式。从代码的可读性来看,迭代器增加了代码的理解成本。不过从“对象”的角度来分析,迭代器其实有助于代码封装维护,因为它将分散的逻辑内聚到一起,并且提供了统一的消费模式。最后,你觉得这个颗糖好吃吗?