go1.23新特性-迭代器
golang1.23发布了,带来新的迭代器类型和其他提升
语法层新特性
迭代器支持iterator
迭代器是用来迭代集合元素的一种抽象,可以屏蔽底层实现的差异。迭代器在大多数流行语言中都有实现,比如C++, Java, Javascript等。在这之前,go没有提供迭代器。不过从go1.23开始,go也有迭代器类型了。
在之前的go版本中,for-range表达式仅支持在slices、arrays、maps和int(go 1.22支持)三种内置的集合类型上和int上使用,不支持用户自定义的集合类型,只能算是一种残废的迭代器。go1.23开始,for-range表达式可以支持遍历函数类型了。简单的说就是go1.23把迭代器规范化为了三种标准函数类型(迭代器入参被称作yield函数):
1
2
3
func(yield func()bool) // 空参迭代
func(yield func(K)bool) // 单参数迭代
func(yield func(k,V)bool) // 双参数迭代
除了上述三个标准的迭代器类型之外,还新增了一种叫做pull的迭代器类型,作为区分,上面的迭代器可以归类为push类型。
所谓push类型,可以理解为迭代的元素是被主动推进yield函数中的
push类型迭代器
标准库为迭代器定义了两个新的类型:Seq和Seq2。分别表示单值序列/双值序列(键值对)迭代器类型:
1
2
3
4
5
6
7
package iter
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
// 目前没有空值序列
比如我们定义一个Set集合类型:
1
2
3
4
// Set holds a set of elements.
type Set[E comparable] struct {
m map[E]struct{}
}
用新的Seq类型,可以这样实现迭代集合中所有值的迭代器:
1
2
3
4
5
6
7
8
9
10
// 获取集合中的所有值
func (s *Set[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
return
}
}
}
}
All函数返回了一个单值迭代器,在函数内部遍历map的key,并调用yield函数,如果yield函数返回false就终止。这里涉及到两个函数,初看容易犯迷糊,简单起见我们可以这么理解:我们返回的迭代器函数是一个生产者,而yield入参函数其实是外部消费者。我们的迭代器就是为入参消费者屏蔽集合元素生产逻辑的。
通过返回标准的迭代器类型,我们就可以直接用for-range来迭代我们自定义的Set集合了。
1
2
3
for v := range s.All() {
fmt.Println(v)
}
在push迭代器中,当yield返回false的时候,我们可能会做一些清理工作。在我们直接使用for-range迭代的时候,标准库实现也会保证当迭代器过早终止时,比如遇到break或者panic等原因,也会触发yield入参返回false。
go官方建议所有集合类型都通过All方法提供一个迭代器,这样调用方就省掉了判断,直接确定All返回的是一个迭代器了。
pull类型迭代器
pull类型迭代器主要是用来并行迭代多个集合。通过它的名字可以帮助我们记住它的工作方式:每pull一次,它就返回一个集合中的元素。
go1.23新增了iter.Pull/iter.Pull2两个函数,它可以帮助我们把push迭代器转换为pull迭代器(next函数),同时提供了一个终止函数,用来触发我们转换的push迭代器中的yield函数返回false。
1
2
3
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())
func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())
pull迭代器 vs push迭代器
- push迭代器是主动将每个元素“推送”到yield入参函数中。支持for-range表达式直接使用。
- pull迭代器是被动的,每次需要外部“拉取”一个元素。不能直接用for-range表达式。
举个例子,实现一个函数判断两个push迭代器是否包含相同的元素且有相同的顺序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func EqSeq[E comparable](s1, s2 iter.Seq[E]) bool {
next1, stop1 := iter.Pull(s1) // 直接调用标准库Pull函数转换为pull迭代器
defer stop1() // 记得触发stop,通知迭代器做清理工作
next2, stop2 := iter.Pull(s2)
defer stop2()
for {
// 并行迭代: 一次迭代两个序列
v1, ok1 := next1()
v2, ok2 := next2()
if !ok1 {
return !ok2
}
if ok1 != ok2 || v1 != v2 {
return false
}
}
}
当迭代器迭代完所有元素时会自动出发yield函数返回false,严格来说这种情况我们不需要再次调用stop函数了。但是习惯性的写上defer stop()会更简单,更安全。
迭代迭代器
迭代器适配器
Filter迭代过滤器: 给迭代器套上一层Filter,生成一个新的迭代器
1
2
3
4
5
6
7
8
9
10
11
func Filter[V any](f func(V) bool, s iter.Seq[V]) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range s {
if f(v) {
if !yield(v) {
return
}
}
}
}
}
标准库目前还没有Filter适配器,没准儿以后会有
二叉树
在二叉树中利用push迭代器非常有效:
1
2
3
4
type Tree[E any] struct {
val E
left, right *Tree[E]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// All 返回一个二叉树元素迭代器
func (t *Tree[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
t.push(yield)
}
}
// push 将所有元素“推入”yield函数中
func (t *Tree[E]) push(yield func(E) bool) bool {
if t == nil {
return true
}
return t.left.push(yield) && // 递归左子树
yield(t.val) && // 当前节点
t.right.push(yield) // 递归右子树,可以看出这是一个中序迭代器
}
新增迭代器函数
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)
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])
标准库使用iterator的例子
返回map中不长于n的值
1
2
3
4
5
6
7
// LongStrings 返回map中不长于n的值,这里用到了上面提到的Filter适配器
func LongStrings(m map[int]string, n int) []string {
isLong := func(s string) bool {
return len(s) >= n
}
return slices.Collect(Filter(isLong, maps.Values(m)))
}
按行处理文件
不适用迭代器的写法如下,这种写法下: bytes.Split会分配byte slices内存并返回,这会加重垃圾收集器的负担。
1
2
3
for _, line := range bytes.Split(data, []byte{'\n'}) {
handleLine(line)
}
使用迭代器能改进这种情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Lines 返回一个迭代器
func Lines(data []byte) iter.Seq[[]byte] {
return func(yield func([]byte) bool) {
for len(data) > 0 {
line, rest, _ := bytes.Cut(data, []byte{'\n'})
if !yield(line) {
return
}
data = rest
}
}
}
// 使用迭代器遍历的代码,更简洁
for _, line := range Lines(data) {
handleLine(line)
}
传递函数到迭代器中
除了使用for-range来遍历迭代器之外,我们当然也可以手写yield函数,当然这种方式也没啥特殊的,只是表明yield函数只是一个普通的函数类型:
1
2
3
4
5
6
func PrintAllElements[E comparable](s *Set[E]) {
s.All()(func(v E) bool {
fmt.Println(v)
return true
})
}
升级到1.23
可以采用四种方式来升级:
go get go@1.23- 编辑go.mod
- 在特定文件上添加构建标记:
//go:build go1.23
go工具改进
go env change命令可以显示出跟默认设置不同的环境变量配置。go mod tidy -diff命令可以只显示要发生的go依赖变更而不会实际去修改go.mod/go.sum。go vet会提示当前go版本不支持的go更新版本符号的错误。
Comments powered by Disqus.