前言
Go 1.23
版本在北京时间 2024 年 8 月 14 日凌晨 1:03 发布。该版本带来了多项重大更新,具体内容可以参考我之前的文章:Go 1.23 版本发布啦,这些重大更新你一定要知道!。本文将重点介绍 iterator
标准迭代器。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
为什么引入标准迭代器
迭代器在 Go
语言中并非新概念,实际上,它一直存在于 Go
的生态系统中。如果你经常使用 Go
标准库,可能已经注意到某些库提供了迭代器的实现,例如:bufio.Scanner、database.Rows、filepath.Walk(Dir)、flag.Visit 和 sync.Map.Range 等。那么为什么 Go
官方仍然会提供统一的迭代器标准呢?主要原因在于现有的迭代器设计和使用方式各不相同。当我们使用一个新库的迭代器时,通常需要学习它的具体使用方法。如果能够统一迭代器的标准化形式,我们只需掌握标准迭代器的定义和使用方式,便可以适应所有迭代器。
迭代器
在 Go 1.23
中,迭代器 实际上是指符合以下三种函数签名之一的函数:
func(yield func() bool)
func(yield func(V) bool)
func(yield func(K, V) bool)
如果一个函数或方法返回的值符合上述形式之一,那么该返回值就可以被称为 迭代器。
迭代器又分为 推迭代器 和 拉迭代器,上面这种设计是典型的 推迭代器,通过调用 yield
函数逐步推出一系列值,yield
函数返回 bool
,决定是否继续执行推出操作。
代码示例:
代码语言:go复制func Backward[E any](s []E) func(yield func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
在这个示例中,该迭代器会倒序遍历 s
切片中的元素,并通过 yield
函数将每个元素推出去。如果 yield
返回 false
,遍历将提前终止。
Range Over Function Types(对函数类型的遍历)
看了前面的迭代器实现后,你是否有一头雾水:迭代器通过调用 yield
函数逐步推出元素值,那么我们该如何接收迭代器推出的值呢?答案是使用 for-range
循环。
在 Go 1.23
版本中,for-range
循环中的范围表达式得到了改进。此前,范围表达式仅支持 array
(数组)、slice
(切片) 和 map
(映射) 等类型,而从 Go 1.23
开始,新增了对函数类型的支持。不过,函数类型必须是前面所提到的三种类型之一,也就是函数需要实现迭代器。
代码示例:
代码语言:go复制package main
import "fmt"
func main() {
s := []string{"程序员", "陈明勇"}
for i, v := range Backward(s) {
fmt.Println(i, v)
}
}
// Backward 倒序迭代
func Backward[E any](s []E) func(yield func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
程序运行结果:
代码语言:bash复制1 陈明勇
0 程序员
iter
包
为了简化迭代器的使用,Go 1.23
版本新增了一个 iter
包,该包定义了两种迭代器类型,分别是 Seq
和 Seq2
,用于处理不同的迭代场景。
package iter
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
Seq
和 Seq2
的区别:
Seq[V any]
undefinedSeq
是一个泛型类型的函数,接收一个yield
函数作为参数。它推出单个元素,例如切片的索引或映射中的键。yield
函数返回bool
,决定是否继续迭代。
使用场景:可以用于返回一个单值的迭代,比如切片中的索引或值,或映射中的键或值。
Seq2[K, V any]
undefinedSeq2
是一个泛型类型的函数,接收一个yield
函数,推送一对元素,例如切片中的索引和值,或者映射中的键值对。yield
函数同样返回bool
,以决定是否继续迭代。
使用场景:当需要同时返回两个值(如键和值)时使用。
在 Set 集合中使用迭代器的案例
代码语言:go复制type Set[E comparable] struct {
m map[E]struct{}
}
func NewSet[E comparable]() Set[E] {
return Set[E]{m: make(map[E]struct{})}
}
func (s Set[E]) Add(e E) {
s.m[e] = struct{}{}
}
func (s Set[E]) Remove(e E) {
delete(s.m, e)
}
func (s Set[E]) Contains(e E) bool {
_, ok := s.m[e]
return ok
}
上面是一个基于泛型的简单 Set
集合实现,该集合包含一个类型为 map[E]struct{}
的 m
字段,用于存储集合元素。Set
集合还提供了 Add
(添加元素)、Remove
(移除)和 Contains
(判断元素是否存在)三个基础方法。
由于 m
字段是未导出的,开发者在其他包中无法直接访问它。如果开发者想遍历集合中的元素,即遍历 m
映射,该如何实现呢?我们可以选择自定义一个迭代器方法,或者返回 m
映射供用户自行遍历。不过,更重要的是,我们可以借助 Go 1.23
提供的迭代器机制来实现这个功能。
func (s Set[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
return
}
}
}
}
完整的代码案例:
代码语言:go复制package main
import (
"fmt"
"iter"
)
type Set[E comparable] struct {
m map[E]struct{}
}
func NewSet[E comparable]() Set[E] {
return Set[E]{m: make(map[E]struct{})}
}
func (s Set[E]) Add(e E) {
s.m[e] = struct{}{}
}
func (s Set[E]) Remove(e E) {
delete(s.m, e)
}
func (s Set[E]) Contains(e E) bool {
_, ok := s.m[e]
return ok
}
func (s Set[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
return
}
}
}
}
func main() {
set := NewSet[string]()
set.Add("程序员")
set.Add("陈明勇")
for v := range set.All() {
fmt.Println(v)
}
}
程序运行结果:
代码语言:bash复制程序员
陈明勇
拉迭代器
在讲 拉迭代器 之前,我们先了解一下 推拉迭代器 两者的区别:
- 推迭代器 将容器中的每个值主动推送到
yield
函数中。在Go
语言中,我们可以通过for-range
循环直接接收被推送的值。 - 与此相反,拉迭代器 则是由调用方主动请求数据。每次调用拉迭代器时,它从容器中拉出下一个值并返回。虽然
for/range
语句不直接支持拉迭代器,但通过普通的for
循环可以轻松实现对拉迭代器的迭代。
值得高兴的是,我们并不需要手动实现一个拉迭代器,因为 iter
包提供了 Pull
函数,该函数接收一个 标准(推)迭代器 类型的参数,返回两个参数,第一个参数是 拉迭代器,第二个参数是 停止 函数。当我们不再需要拉取元素的时候,调用 停止 函数即可。
代码示例:
代码语言:go复制package main
import (
"fmt"
"iter"
)
type Set[E comparable] struct {
m map[E]struct{}
}
func NewSet[E comparable]() Set[E] {
return Set[E]{m: make(map[E]struct{})}
}
func (s Set[E]) Add(e E) {
s.m[e] = struct{}{}
}
func (s Set[E]) Remove(e E) {
delete(s.m, e)
}
func (s Set[E]) Contains(e E) bool {
_, ok := s.m[e]
return ok
}
func (s Set[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
return
}
}
}
}
func main() {
set := NewSet[string]()
set.Add("程序员")
set.Add("陈明勇")
next, stop := iter.Pull(set.All())
for {
v, ok := next()
if !ok {
break
}
fmt.Println(v)
stop()
}
}
主动调用 stop
函数会导致 yield
函数提前返回 false
,因此通过 next
函数获取到的 ok
也将为 false
,表示迭代结束,无法再继续获取元素。
程序运行结果:
代码语言:bash复制程序员
标准库新增的迭代器函数
随着迭代器的引入,slices
和 maps
包也新增了一些与迭代器一起使用的函数。
slices
包新增的函数:
All([]E) iter.Seq2[int, E]
Values([]E) iter.Seq[E]
Collect(iter.Seq[E]) []E
AppendSeq([]E, iter.Seq[E]) []E
Backward([]E) iter.Seq2[int, E]
Sorted(iter.Seq[E]) []E
SortedFunc(iter.Seq[E], func(E, E) int) []E
SortedStableFunc(iter.Seq[E], func(E, E) int) []E
Repeat([]E, int) []E
Chunk([]E, int) iter.Seq([]E)
关于这些函数的用法,可参考 slices 包文档
maps
包新增的函数:
All(map[K]V) iter.Seq2[K, V]
Keys(map[K]V) iter.Seq[K]
Values(map[K]V) iter.Seq[V]
Collect(iter.Seq2[K, V]) map[K, V]
Insert(map[K, V], iter.Seq2[K, V])
关于这些函数的用法,可参考 maps 包文档
小结
本文详细介绍了 Go 1.23
版本中的迭代器。内容涵盖了引入 标准迭代器 的主要原因、迭代器的定义及其使用方法。此外,还讨论了 iter
包的功能扩展,以及 slices
和 maps
标准库中新增的与迭代器相关的函数。
有人认为,引入迭代器使 Go
变得更加复杂,因为迭代器的代码实现可能会影响可读性。对于刚接触 Go
迭代器的开发者来说,确实可能感到有些不适应。不过,Go
官方为了简化迭代器的使用,新增了 iter
包,并在 slices
和 maps
包中提供了许多便捷函数,以提升开发体验。
总的来说,引入 标准迭代器 统一了迭代器的设计和使用方式,解决了各自为政的问题,进一步优化了 Go
的生态系统。
你对迭代器有什么看法?欢迎在评论区留言探讨。
参考资料
Range Over Function Types
推荐阅读
Go 1.23 版本发布啦,这些重大更新你一定要知道!
Go 1.23 新特性:Timer 和 Ticker 的重要优化
Go 1.23 新特性:slices 和 sync 等核心库的微调,大幅提升开发体验
你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。
成功的路上并不拥挤,有没有兴趣结个伴?
关注我,加我好友,一起学习一起进步!