Go1.23 新特性:争议最大的 iter 迭代器,可遍历万物!

Go1.23 的迭代器引入,对于 Go 来讲是一个重要的里程碑。虽然在社区上引来了国外社区的大量争议。但也带来了 for-loop 的完整体系的建设,提供了迭代器可遍历万物的概念。
首页 新闻资讯 行业资讯 Go1.23 新特性:争议最大的 iter 迭代器,可遍历万物!

大家好,我是煎鱼。

Go1.23 新版本中,在发布过程中争议最大的新特性莫过于:迭代器(iterators)。

原本计划先写一个这个 proposal 的提出背景的,但没想到,迭代器涉及的到 proposal 比较多,而且是由 rsc 亲自负责。

总感觉 rsc 早有预谋,在 Go1.23 蓄力一击,搞完就撤了。

Go1.23 新特性:迭代器

提出过程

我能翻到的最早明确提出要加迭代器是在 discussions/54245[1] 中进行了广泛讨论:

图片图片

随后折腾了许久,最终 rsc 牵头在 discussions/56413[2] 做了初步敲定:

图片图片

后面今年 《spec: add range over int, range over func》[3],包含在 for-range int 和 function 中再次冲击新特性:

图片图片

我就不一一列举和解释了。大家可以理解为比较折腾高密度讲了很久。

为什么要做

根据 Go 官方几个 issues 和 discussions 的说法,汇总一下。具体缘由如下:

  1. 其他编程语言有提供:大多数变成语言都提供了使用迭代器接口遍历存储在容器中的值的标准化方法。

  2. Go 就差迭代器没提供了:Go 提供了可用于 map、slices、stings、 array 和 channel 的 for range,但没有为用户编写的容器提供任何通用机制,也没有提供迭代器接口。

  3. 现在大家都各自为政:社区和官方最终采用了各种各样的方法去实现类似功能,每种实现都采用了在当时情况下最合理的方法,但各自为政的决定却给用户带来了许多困惑。

“容器” 指代的是什么

有同学会疑惑第一点中提到的容器是什么?

实际上指代的是:使用迭代器 “提供一种按顺序访问聚合对象元素的方法,而无需暴露其底层表现”。

这句话中所说的聚合对象就是上文中所提到的容器。聚合对象或容器只是一个包含其他值的值。

Go 标准库里的各自实现

具体 Go 标准库中各自为政的。例如:

  • runtime.CallersFrames:Frames.Next 方法。

  • bufio.Scanner:Scanner.Scan 方法。

  • database/sql.Rows:Rows.Scan 和配套 Rows.Next 方法。

有兴趣的可以自己看一下函数调用或实现。

平时写业务代码都会接触到。这里就不深入展开了。

Go1.23 迭代器介绍

功能说明

在 Go 1.23 中,将会同时支持用户定义容器类型的 for-range 和标准化形式的迭代器。

本次新版本中:

  • 扩展了 for/range 语句,使其支持对函数类型的取值范围。

  • 添加了标准库类型和函数,以支持将函数类型用作迭代器。

后续通过新增的迭代器的标准定义,我们编写的函数可以顺利地与不同的容器类型配合使用。

有种可以循环遍历万物的感觉。

迭代器的快速例子

以下是 Go1.23 中迭代器的一些基础的标准例子。

分别包含:单值迭代器和二值迭代器。

前置知识:yield

在 Go 中,yield 关键字的引入使得函数可以像迭代器一样工作。这一特性是在 Go 1.22 版本中被提出的,允许函数在执行过程中暂时挂起,并返回一个或多个值。

这种机制与其他编程语言(如:Python)中的 yield 关键字有些相似,但在 Go 中实现的方式有所不同。

以下是关于 Go 中 yield 关键字的一些关键点:

  • 功能:yield 关键字使得函数能够在执行时返回一个或多个值,并在下次调用时从上次返回的地方继续执行。这样可以有效地处理大量数据而不需要一次性加载所有数据。

  • 用法:在 Go 中,yield 并不是一个独立的关键字,而是作为一种函数参数的形式出现。具体来说,函数可以接受一个 yield 函数作为参数,该函数负责接收生成的值并返回一个布尔值,指示是否继续迭代。

例子一:单值迭代器(iter.Seq)

示例代码如下:

import("fmt""iter")func Stat(vint)iter.Seq[int]{returnfunc(yield func(int)bool){fori :=v;i>=0;i-- {if!yield(i){return}
  }
 }
}

func main(){forv :=range Stat(11){
  fmt.Println(v)}
}

输出结果:

11109876543210

例子二:二值迭代器(iter.Seq2)

示例代码如下:

func Backward[Eany](s[]E"E any")iter.Seq2[int,E]{returnfunc(yield func(int,E)bool){fori :=len(s)-1;i>=0;i-- {if!yield(i,s[i]){return}
  }
 }
}

func main(){
 sl :=[]string{"脑子","进","煎鱼","了"}fori,s :=range Backward(sl){
  fmt.Printf("%d: %s\n",i,s)}
}

输出结果:

3: 了2: 煎鱼1: 进0: 脑子

标准库内的迭代器使用

slices

本次 Go1.23 在 slices 标准库中针对迭代器,新增了:slices.All、slices.Values、slices.Collect 方法。

函数签名如下:

funcAll[Slice~[]E,Eany](s Slice"Slice ~[]E, E any")iter.Seq2[int,E]funcValues[Slice~[]E,Eany](s Slice"Slice ~[]E, E any")iter.Seq[E]func Collect[Eany](seq iter.Seq[E]"E any")[]E

示例代码如下:

func main(){
 s1 :=[]int{1,2,3}fork,v :=range slices.All(s1){
  fmt.Println("k:",k,"v:",v)}forv :=range slices.Values(s1){
  fmt.Println(v)}// slices.Collect 会将迭代器中的值收集到一个新的切片中并返回它s2 :=slices.Collect(slices.Values([]int{1,2,3}))fmt.Println(s2)}

输出结果:

k:0v:1k:1v:2k:2v:3123[123]

maps

maps 标准库中针对迭代器,新增了:maps.All、maps.Keys、maps.Values、 方法。

函数签名如下:

funcAll[Map~map[K]V,K comparable,Vany](m Map"Map ~map[K]V, K comparable, V any")iter.Seq2[K,V]funcKeys[Map~map[K]V,K comparable,Vany](m Map"Map ~map[K]V, K comparable, V any")iter.Seq[K]funcValues[Map~map[K]V,K comparable,Vany](m Map"Map ~map[K]V, K comparable, V any")iter.Seq[V]

示例代码如下:

func main(){
 m :=map[string]int{"脑子":1,"进":2,"煎鱼":3,"了":4,"吗":5,}fork,v :=range maps.All(m){
  fmt.Println("k:",k,"v:",v)}fork :=range maps.Keys(m){
  fmt.Println(k)}forv :=range maps.Values(m){
  fmt.Println(v)}
}

输出结果:

// maps.Allk: 吗 v:5k: 脑子 v:1k: 进 v:2k: 煎鱼 v:3k: 了 v:4// maps.Keys脑子
进
煎鱼
了
吗// maps.Values34512

总结

Go1.23 的迭代器引入,对于 Go 来讲是一个重要的里程碑。虽然在社区上引来了国外社区的大量争议。但也带来了 for-loop 的完整体系的建设,提供了迭代器可遍历万物的概念。

13    2024-09-09 08:56:03    Go 迭代器 Go1.23