万字长文:彻底掌握 Go 1.23 中的迭代器——使用篇

本文带大家一起来深入探究一下 Go 1.23 中发布的迭代器特性,这是一篇迟来的文章,再不写这篇文章 Go 1.25 就发布了 :),Go 1.25 预计将于 2025 年 8 月发布。

由于篇幅过长,所以文章拆分成了上下两篇发布,本文为上篇------使用篇。下篇为------原理篇,记得来读。

何为迭代器

维基百科对迭代器的定义如下:

迭代器(英语:iterator),是使用户可在容器对象(container,例如链表或数组)上遍访的对象,设计人员使用此接口无需关心容器对象的内存分配的实现细节。其行为很像数据库技术中的光标(cursor),迭代器最早出现在1974年设计的CLU编程语言中。

在各种语言实现迭代器的方式皆不尽同,有些面向对象语言像Java、C#、Ruby、Python、Delphi都已将迭代器的特性内置语言当中,完美的跟语言集成,我们称之隐式迭代器。但像是C++语言本身就没有迭代器的特色,但STL仍利用模板实现了功能强大的迭代器。STL容器的数据的内存地址可能会重新分配(reallocate),与容器绑定的迭代器仍然可以定位到重新分配后的正确的内存地址。

虽然这段对迭代器的定义比较晦涩,但可以看出,许多主流编程语言都原生支持迭代器。而 Go 也终于在 1.23 中支持了迭代器特性,这是继泛型特性以来 Go 在语法层面上的又一次重大更新。

我们再来看看 Go 官方是如何定义迭代器的,Go 官方在 1.23 版本新增的 iter 包文档 中对于迭代器定义如下:

An iterator is a function that passes successive elements of a sequence to a callback function, conventionally named yield. The function stops either when the sequence is finished or when yield returns false, indicating to stop the iteration early.

翻译如下:

迭代器是一个将序列的连续元素传递给回调函数(通常命名为 yield)的函数。该函数在序列结束或 yield 返回 false(表示提前停止迭代)时停止。

看到这个定义,你可能还不是很理解,不过没关系,你现在只需要记得迭代器是一个函数即可,接下来我们将通过此文彻底掌握 Go 迭代器。

为什么要引入迭代器

discussions/56413 中 Go 团队技术负责人 rsc 对 Go 中为什么要引入迭代器做了诠释:

In the standard library alone, we have archive/tar.Reader.Next, bufio.Reader.ReadByte, bufio.Scanner.Scan, container/ring.Ring.Do, database/sql.Rows, expvar.Do, flag.Visit, go/token.FileSet.Iterate, path/filepath.Walk, go/token.FileSet.Iterate, runtime.Frames.Next, and sync.Map.Range, hardly any of which agree on the exact details of iteration. Even the functions that agree on the signature don't always agree about the semantics. For example, most iteration functions that return (T, bool) follow the usual Go convention of having the bool indicate whether the T is valid. In contrast, the bool returned from runtime.Frames.Next indicates whether the next call will return something valid.

大概意思是说,仅标准库中,就包含这么多迭代器函数,并且这些函数接口并未统一,各自为政。甚至有些迭代器即使在函数签名上达成了一致,在语义上却各不相同。这就使得我们学习 Go 代码的成本提高了,而且与 Go 语言面向工程的简洁特点相悖。

于是,这个由 rsc 牵头,社区中争议很大的特性在 Go 1.23 中落地了。

这里举几个例子,你可以先来感受一下 Go 语言在未提供迭代器特性时的现状:

go 复制代码
// bufio.Reader
r := bufio.NewReader(...)
for {
    line, _ , err := r.ReadLine()
    if err != nil {
        break
    }
    // do something
}


// bufio.Scanner
scanner := bufio.NewScanner(...)
for scanner.Scan() {
    line := scanner.Text()
    // do something
}


// database/sql.Rows
rows, _ := db.QueryContext(...)
for rows.Next() {
    if err := rows.Scan(...); err != nil {
        break
    }
    // do something
}

可以发现,Go 语言中迭代器函数的设计非常混乱,接口设计各不相同,这些设计可能都是当时的最佳实践,但是接口不统一的事实,确实存在问题,急需一个统一的标准来解决此问题。

所以,其实迭代器在 Go 语言中并不是什么新鲜的东西,它们一直存在,只不过各个迭代器函数实现接口并不统一。这个问题早期也许不明显,但随着 Go 语言标准库功能的增多以及泛型特性的引入,越来越多的泛型集合实现,也都需要设计迭代器接口。因此,语法层面的迭代器特性呼之欲出。

现在,你大概理解什么是迭代器了吗?你可以简单的,将能够作用于 for 循环,并不断产生下一个值的对象,称为迭代器。

迭代器示例

我们已经对迭代器有了初步的认识,为了加深你对 Go 中迭代器的理解,现在我们一起来自己实现一下迭代器。

迭代器模式

Go 是现代语言中的新贵,虽然与主流面向对象语言在设计上有所出入,不过 Go 也支持通过设计模式中的迭代器模式来实现迭代器特性。

示例如下:

go 复制代码
package main

import "fmt"

type Iterator struct {
	data  []int
	index int
}

func NewIterator(data []int) *Iterator {
	return &Iterator{data: data, index: 0}
}

func (it *Iterator) HasNext() bool {
	return it.index < len(it.data)
}

func (it *Iterator) Next() int {
	if !it.HasNext() {
		panic("Stop iteration")
	}
	value := it.data[it.index]
	it.index++
	return value
}

func main() {
	it := NewIterator([]int{0, 1, 2, 3, 4})
	for it.HasNext() {
		fmt.Println(it.Next())
	}
}

这是 Go 语言实现的简化版本的迭代器模式,NewIterator 创建并返回一个迭代器对象,it.HasNext() 返回迭代器中是否还有下一个值,刚好可以作用于 for 循环对其进行遍历,it.Next() 返回迭代器中的下一个值。

回调函数风格

此外,因为 Go 语言支持高阶函数,即函数可以作为另一个函数的参数,所以我们还可以通过回调函数的方式,来实现迭代器。

示例如下:

go 复制代码
package main

import (
	"container/ring"
	"fmt"
)

func main() {
	// 循环链表
	r := ring.New(5)
    // 初始化链表
	for i := 0; i < r.Len(); i++ {
		r.Value = i  // 为当前节点赋值
		r = r.Next() // 移动到下一个节点
	}
    // 迭代器
	r.Do(func(v any) {
		fmt.Println(v)
	})
}

这里引入了标准库中的 container/ring 包,这个包实现了循环链表,它定义了 *Ring.Do 方法来对链表 Ring 对象进行遍历操作。

Do 方法按正向顺序对链表中的每个元素调用函数 f,其实现如下:

go 复制代码
// Do calls function f on each element of the ring, in forward order.
// The behavior of Do is undefined if f changes *r.
func (r *Ring) Do(f func(any)) {
	if r != nil {
		f(r.Value)
		for p := r.Next(); p != r; p = p.next {
			f(p.Value)
		}
	}
}

可以看到,Do 方法内部有一个 for 循环,不停的调用 r.Next() 来获取链表中的下一个值,然后调用回调函数 f 并将下一个值传递给它。

Go 风格迭代器

以上两种方式实现的迭代器,在其他主流编程语言中也很容易复刻。现在我们再来使用 channel 数据结构实现一种 Go 语言独有风格的迭代器。

示例如下:

go 复制代码
package main

import "fmt"

func generator(n int) <-chan int {
	ch := make(chan int)
	go func() {
		for i := 0; i < n; i++ {
			ch <- i
		}
		close(ch)
	}()
	return ch
}

func main() {
	for n := range generator(5) {
		fmt.Println(n)
	}
}

这里我们定义了一个生成器函数 generator,它返回一个只读的 <-chan int。而 channel 类型恰好能够作用于 for-range 循环,生成器 generator 不断产生下一个值,for-range 循环就能接收到,这样我们就实现了 Go 风格的迭代器。

虽然这个版本的迭代器非常具有 Go 风格,但因为使用了 channel 所以性能较差,不建议作为实现迭代器的首选方案。

有了前面的铺垫,接下来我们就要真正进入到 Go 语言官方推出的迭代器的学习了。

何时引入迭代器

Go 引入迭代器的过程可谓一波多折,在 2020 年 8 月的 issues/40605 中就有人为 Go 2 提出了迭代器提案,之后又经历了在 issues/43557discussions/54245discussions/56413issues/61405issues/61897 中多次讨论。终于,迭代器特性以实验性质被加入到 Go 1.22 版本中,通过 GOEXPERIMENT=rangefunc 编译参数可以在 Go 中启用 range-over-function 迭代器特性。此外,rsc 还在 issues/61898 中提出了 golang.org/x/exp/xiter 包的提案,用于适配迭代器,不过这个提案最终被撤销了。

2024 年 8 月,随着 Go 1.23 版本的发布,Go 迭代器也终于正式落地了。同时引入了 iter 包,来为用户自定义迭代器提供便利。

这便是 Go 引入迭代器的主要时间脉络。

改变

Go 语言发布之初,for-range 循环就能够支持遍历 arrayslicestringmapchannel 几种内置类型。

在 Go 1.22 中,for-range 新增了对整数类型值(integer value)的支持,比如 for i := range 10 {...}

而在 Go 1.23 中,Go 引入了迭代器特性,for-range 又新增了对如下三种特定函数类型的支持:

go 复制代码
// 无返回值迭代器,函数 f 签名为:func(func() bool)
for range f {...}

// 返回一个值迭代器,函数 f 签名为:func(func(V) bool)
for x := range f { ... }

// 返回两个值迭代器,函数 f 签名为:func(func(K, V) bool)
for x, y := range f { ... }

现在,for-range 支持遍历的所有类型如下:

go.dev/ref/spec#Fo...

go 复制代码
Range expression                                       1st value                2nd value

array or slice      a  [n]E, *[n]E, or []E             index    i  int          a[i]       E
string              s  string type                     index    i  int          see below  rune
map                 m  map[K]V                         key      k  K            m[k]       V
channel             c  chan E, <-chan E                element  e  E
integer value       n  integer type, or untyped int    value    i  see below
function, 0 values  f  func(func() bool)
function, 1 value   f  func(func(V) bool)              value    v  V
function, 2 values  f  func(func(K, V) bool)           key      k  K            v          V

至此,Go 语言 for-range 语句支持的可迭代对象类型完整拼图已经形成,真正实现了"万物皆可遍历"的能力。

迭代器使用示例

基于以上的讲解,你可能对 Go 迭代器还是有些不明所以,那么现在,咱们一起从代码层面,切身感受一下迭代器的存在。我将通过几个示例代码,带你体验迭代器的用法。

迭代器实现

最简单的迭代器

我们先来实现一个最简单的迭代器。

示例如下:

go 复制代码
package main

import (
	"fmt"
)

func iterator(yield func() bool) {
	for i := 0; i < 5; i++ {
		if !yield() {
			return
		}
	}
}

func main() {
	i := 0
	for range iterator {
		fmt.Printf("i=%d\n", i)
		i++
	}
}

iterator 函数就是一个迭代器,它接收一个 yield 函数作为参数,并且函数签名符合 func(func() bool) 类型,那么 iterator 就能够作用于 for-range 循环。

iterator 函数内部,我们启动了一个 for 循环,这个循环将会迭代 5 次。并且在 for 循环代码块内部,每次迭代都会调用 yield() 函数,并判断其返回值,如果返回 false,则直接使用 return 提前终止循环。

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
i=0
i=1
i=2
i=3
i=4

这个示例看起来有点傻,但这确实已经是 Go 中最简单的迭代器实现了。

你也许比较疑惑,这个示例代码的执行逻辑是什么?咱们暂且不去深究原理,挖一个坑留在这里,稍后再来填上。

现在,你只需要知道这段代码大概按照如下流程来执行:

  • 首先,符合函数签名 func(func() bool) 的函数,都可以作为迭代器,所以 iterator 是一个迭代器,可以应用于 for-range 循环。
  • 接着,当 for-range 开始迭代时,迭代器函数 iterator 会被调用,并执行其内部代码。这里你需要注意,iterator 函数只会被调用一次。
  • 现在重点来了,iterator 函数接收一个 yield 函数作为参数,那么这个 yield 函数是哪里来的呢?其实它是 Go 编译器帮我们自动生成的函数,并自动作为参数传递给 iteratoryield 函数内部包含了 for-range 循环体中的代码,你可以简单的将 yield 函数理解为如下伪代码:
go 复制代码
func yield() bool {
    fmt.Printf("i=%d\n", i)
    i++
    return true
}
  • iterator 函数内部,有一个 for 循环,将执行 5 次。每一次循环都会调用 yield 函数,并根据其返回值决定是否停止迭代。
  • for 循环一旦结束,iterator 函数便执行完成,即 for-range 迭代完成。

现在,是不是对迭代器的认知更清晰了一些呢?

控制迭代次数

刚刚的迭代器实现过于简单,甚至都无法从外部控制迭代次数,为了控制迭代次数,我们可以给 iterator 再嵌套一层函数。

示例如下:

go 复制代码
package main

import (
	"fmt"
)

func iterator(n int) func(yield func() bool) {
	return func(yield func() bool) {
		for i := 0; i < n; i++ {
			if !yield() {
				return
			}
		}
	}
}

func main() {
	i := 0
	for range iterator(3) {
		fmt.Printf("i=%d\n", i)
		i++
	}
}

这里为 iterator 又嵌套了一层函数,这样就可以通过参数的形式控制迭代次数了。

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
i=0
i=1
i=2

值得注意的是,现在 for-range 遍历迭代器时,不再是直接使用函数名称 for range iterator 的形式进行迭代,而是要调用 iterator 函数以 for range iterator(3) 的形式来迭代。所以,实际上 iterator(3) 的返回值,才是真正的迭代器函数。

这个示例代码的套路是不是有点熟悉?用 Go 写过 Web 程序的读者应该能够体会得到,比如我们在用 Gin 框架开发项目时,编写的中间件程序就经常使用这种套路。如开源项目 miniblog 中的 AuthnMiddleware 中间件实现。

输出一个值

我们已经展示了两个迭代器示例,但是,它们都不产生值,在使用 for-range 进行迭代时,真的就是纯迭代,for-range 接收不到任何循环变量,所以感觉也没啥大用。

那么我们再来实现一个能够在每轮迭代时输出一个值的迭代器。

示例如下:

go 复制代码
package main

import (
	"fmt"
)

func iterator(n int) func(yield func(v int) bool) {
	return func(yield func(v int) bool) {
		for i := 0; i < n; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	i := 0
	for v := range iterator(10) {
		if i >= 5 {
			break
		}
		fmt.Printf("%d => %d\n", i, v)
		i++
	}
}

这个版本的迭代器函数签名已经变了,不再是 func(func() bool),而变成了 func(func(V) bool)。即 yield 函数支持接收一个参数 v int

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
0 => 0
1 => 1
2 => 2
3 => 3
4 => 4

现在这个示例程序终于看起来有点意义了,像那么回事了。

输出两个值

讲到这里,想必你已经猜到接下来我们要实现的迭代器功能了。

没错,for-range 在迭代容器类型数据结构时,是支持接收两个循环变量的。比如迭代 slice 时会产生 indexvalue 两个循环变量,迭代 map 时会产生 keyvalue 两个循环变量。

我们先来实现支持迭代 slice 类型的迭代器。

示例如下:

go 复制代码
package main

import (
	"fmt"
)

func iterator(slice []int) func(yield func(i, v int) bool) {
	return func(yield func(i int, v int) bool) {
		for i, v := range slice {
			if !yield(i, v) {
				return
			}
		}
	}
}

func main() {
	s := []int{0, 1, 2, 3, 4}
	for i, v := range iterator(s) {
		if i == 2 {
			continue
		}
		fmt.Printf("%d => %d\n", i, v)
	}
}

这个迭代器函数签名为 func(func(K, V) bool),即支持输出两个值,分别是 slice 对象索引(i)和元素(v)。

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
0 => 0
1 => 1
3 => 3
4 => 4

根据输出,可以发现我们正确的得到了 indexvalue。并且,continue 语句也可以正常应用于迭代器。

迭代 map

最后,我们再来来实现一下支持迭代 map 类型的迭代器。

示例如下:

go 复制代码
package main

import (
	"fmt"
)

func iterator(m map[string]int) func(yield func(k string, v int) bool) {
	return func(yield func(k string, v int) bool) {
		for k, v := range m {
			if !yield(k, v) {
				return
			}
		}
	}
}

func main() {
	m := map[string]int{
		"a": 0,
		"b": 1,
		"c": 2,
	}
	for k, v := range iterator(m) {
		fmt.Printf("%s: %d\n", k, v)
	}
}

可以发现,这里其实并没有什么新的内容,甚至迭代器函数签名都没变 func(func(K, V) bool),只不过 KV 的类型变了。只需简单改造,我们就已经实现了支持迭代 map 类型的迭代器。

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
b: 1
c: 2
a: 0

本小结,我们一起实现了几个迭代器。这些迭代器看起来有点用,但好像也没那么有用。而且,仅从示例程序来看,我们好像把事情变得更加复杂了。

如你所见,目前来看的确如此。比如迭代 slice 对象,只需要用 for i, v := range slice 即可,为什么还需要构造一个迭代器呢?

咱们接着往下看,你会得到答案。

泛型版本

初次接触 Go 迭代器时,你可能也和我一样,有点云里雾里,Get 不到其用意。

现在,我们使用泛型来重新实现一遍这几个迭代器函数,相信你会对迭代器的用途有更深入的理解。

输出零个值

以下是泛型版本最简单的迭代器实现。

示例如下:

go 复制代码
package main

import (
	"fmt"
)

type Seq0 func(yield func() bool)

func iter0[Slice ~[]E, E any](s Slice) Seq0 {
	return func(yield func() bool) {
		for range s {
			if !yield() {
				return
			}
		}
	}
}

func main() {
	s1 := []int{1, 2, 3}
	i := 0
	for range iter0(s1) {
		fmt.Printf("i=%d\n", i)
		i++
	}

	fmt.Println("--------------")

	s2 := []string{"a", "b", "c"}
	i = 0
	for range iter0(s2) {
		fmt.Printf("i=%d\n", i)
		i++
	}
}

为了让代码可读性更好一些,我将迭代器函数签名定义为 Seq0 类型,命名中的 0 表示迭代器输出零个值。

为了体现泛型的作用,这里还分别演示了迭代 intstring 两种类型的 slice 对象 s1s2

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
i=0
i=1
i=2
--------------
i=0
i=1
i=2
输出一个值

同样是迭代 slice 对象,输出一个值的迭代器实现如下:

go 复制代码
package main

import (
	"fmt"
)

type Seq1[V any] func(yield func(V) bool)

func iter1[Slice ~[]E, E any](s Slice) Seq1[E] {
	return func(yield func(E) bool) {
		for _, v := range s {
			if !yield(v) {
				return
			}
		}
	}
}

func main() {
	s1 := []int{1, 2, 3}
	for v := range iter1(s1) {
		fmt.Printf("v=%d\n", v)
	}

	fmt.Println("--------------")

	s2 := []string{"a", "b", "c"}
	for v := range iter1(s2) {
		fmt.Printf("v=%s\n", v)
	}
}

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
v=1
v=2
v=3
--------------
v=a
v=b
v=c
输出两个值

进一步,输出两个值的迭代器实现如下:

go 复制代码
package main

import (
	"fmt"
)

type Seq2[K, V any] func(yield func(K, V) bool)

func iter2[Slice ~[]E, E any](s Slice) Seq2[int, E] {
	return func(yield func(int, E) bool) {
		for i, v := range s {
			if !yield(i, v) {
				return
			}
		}
	}
}

func main() {
	s1 := []int{1, 2, 3}
	for i, v := range iter2(s1) {
		fmt.Printf("%d=%d\n", i, v)
	}

	fmt.Println("--------------")

	s2 := []string{"a", "b", "c"}
	for i, v := range iter2(s2) {
		fmt.Printf("%d=%s\n", i, v)
	}
}

执行示例代码,得到输出如下:

bash 复制代码
$ go run main.go
0=1
1=2
2=3
--------------
0=a
1=b
2=c

看到这几个泛型版本迭代器的实现,是不是觉得 Go 的迭代器设计确实有点用。针对某种数据结构,我们只需要编写一个迭代器函数,就能迭代此数据结构类型的所有对象。

现在,你知道如何实现泛型版本的 map 迭代器了吗?这个实现就留给你自行去完成了。

本小结最后提醒一下,其实迭代器中的 yield 并非关键字,你可以随意命名,这是只是 Go 官方推荐的约定俗成的名字。

iter 包

前文中,我提到过,Go 随着 1.23 迭代器的发布,新增了 iter 包,而新的 iter 包为用户操作自定义迭代器提供了基础定义。

如下这两个函数类型,就是 iter 包提供的:

go.dev/doc/go1.23#...

go 复制代码
type Seq[V any] func(yield func(V) bool)

type Seq2[K, V any] func(yield func(K, V) bool)

也就是说,其实我们在前文中实现的迭代器,有更便捷的写法。我们无需自己定义 SeqSeq2 类型,iter 包已经为我们定义好了。我们只需要拿过来用就行了。

示例如下:

go 复制代码
package main

import "iter"

func iter1[Slice ~[]E, E any](s Slice) iter.Seq[E] {
	return func(yield func(E) bool) {
		for _, v := range s {
			if !yield(v) {
				return
			}
		}
	}
}

func iter2[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
	return func(yield func(int, E) bool) {
		for i, v := range s {
			if !yield(i, v) {
				return
			}
		}
	}
}

此外,iter 包还提供了两个函数 PullPull2,这两个函数签名如下:

go 复制代码
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())

现在我们还无需知道它们是干什么的,稍后会有专门的小节讲解。

Go 1.23 除了新增 iter 包,也对原有的 slices 包和 maps 进行了增强,为二者新增了很多函数方便与迭代器一起工作。

slices 包

slices 包添加了如下几个与迭代器一起工作的函数:

  • All 返回一个遍历切片索引和值的迭代器。
  • Values 返回一个遍历切片元素的迭代器。
  • Backward 返回一个反向遍历切片的迭代器(从末尾向开头遍历)。
  • Collect 将迭代器中的值收集到一个新切片中。
  • AppendSeq 将迭代器中的值追加到现有切片中。
  • Sorted 将迭代器中的值收集到新切片,并排序该切片。
  • SortedFunc 功能同 Sorted,但支持自定义比较函数
  • SortedStableFunc 功能同 SortedFunc,但使用稳定排序算法(保持相等元素的原始顺序)。
  • Chunk 返回一个遍历切片中连续子切片 的迭代器,每个子切片最多包含 n 个元素。

源码如下:

github.com/golang/go/b...

go 复制代码
package slices

import (
	"cmp"
	"iter"
)

// All returns an iterator over index-value pairs in the slice
// in the usual order.
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
	return func(yield func(int, E) bool) {
		for i, v := range s {
			if !yield(i, v) {
				return
			}
		}
	}
}

// Backward returns an iterator over index-value pairs in the slice,
// traversing it backward with descending indices.
func Backward[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] {
	return func(yield func(int, E) bool) {
		for i := len(s) - 1; i >= 0; i-- {
			if !yield(i, s[i]) {
				return
			}
		}
	}
}

// Values returns an iterator that yields the slice elements in order.
func Values[Slice ~[]E, E any](s Slice) iter.Seq[E] {
	return func(yield func(E) bool) {
		for _, v := range s {
			if !yield(v) {
				return
			}
		}
	}
}

...

可以发现,这里的 All 函数其实就是我们在前文中实现的 iter2 迭代器,Values 函数就是 iter1 迭代器。

这里只粘贴了部分源码,更多实现,就靠你自己去研究了。你还可以查看 pkg.go.dev/slices@go1.... 文档,文档中有每一个迭代器使用示例。

maps 包

maps 包添加了如下几个与迭代器一起工作的函数:

  • All 返回一个遍历映射中键值对的迭代器。
  • Keys 返回一个遍历映射中的迭代器。
  • Values 返回一个遍历映射中的迭代器。
  • Insert 将迭代器中的键值对添加到现有 map 中。
  • Collect 将迭代器中的键值对收集到一个新的 map 中并返回。

源码如下:

github.com/golang/go/b...

go 复制代码
package maps

import "iter"

// All returns an iterator over key-value pairs from m.
// The iteration order is not specified and is not guaranteed
// to be the same from one call to the next.
func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V] {
	return func(yield func(K, V) bool) {
		for k, v := range m {
			if !yield(k, v) {
				return
			}
		}
	}
}

// Keys returns an iterator over keys in m.
// The iteration order is not specified and is not guaranteed
// to be the same from one call to the next.
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K] {
	return func(yield func(K) bool) {
		for k := range m {
			if !yield(k) {
				return
			}
		}
	}
}

// Values returns an iterator over values in m.
// The iteration order is not specified and is not guaranteed
// to be the same from one call to the next.
func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V] {
	return func(yield func(V) bool) {
		for _, v := range m {
			if !yield(v) {
				return
			}
		}
	}
}

...

这里的 All 函数就是我在前文中让你自行去实现的能够支持迭代 map 类型的迭代器。

对于 maps 包更深入的学习,你还可以查看 pkg.go.dev/maps@go1.23... 文档,文档中有每一个迭代器使用示例。

现在,你对 Go 迭代器的理解是否又深入了一层呢?

有如下示例:

go 复制代码
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	s := []string{"a", "b", "c"}
	for i, v := range slices.All(s) {
		fmt.Printf("%d => %s\n", i, v)
	}

	m := map[string]int{
		"a": 0,
		"b": 1,
		"c": 2,
	}
	for k, v := range maps.All(m) {
		fmt.Printf("%s: %d\n", k, v)
	}
}

看到这段代码,是不是感觉对迭代器的作用更加直观了。无论是迭代 slice 还是 map 类型,我们都只需要调用 All 函数,即可完成迭代。

Go 迭代器统一了这两种容器类型的迭代 API,后续如果再有容器类型需要迭代,都可以像这样实现一个 All 函数。这降就低了我们开发者学习不同数据结构的迭代 API 的心智负担。这也有助于形成共识,对用户培养相同的习惯,以后在对 Go 中任意类型的迭代,就成了一个很平常的操作,所有接口都符合直觉。

理想很丰满,现实有点骨感,让我们一起期待那一天的到来。

未完待续。

下篇:《彻底掌握 Go 1.23 中的迭代器------原理篇》

联系我

相关推荐
用户15129054522010 分钟前
Docker部署 Alist
后端
白应穷奇11 分钟前
Diesel的高性能特性: 深入理解Rust ORM的性能优化
后端·rust
用户15129054522011 分钟前
HDOJ-ACM1017(JAVA)
后端
Ray6611 分钟前
OOP 四大特征
后端
汪子熙1 小时前
如何使用 Node.js 代码下载 Github issue 到本地
javascript·后端
拾光拾趣录1 小时前
🔥99%人只知WebP小,第3个特性却翻车?💥
前端·面试
冒泡的肥皂1 小时前
2PL-事务并发(二
数据库·后端·mysql
xiaok1 小时前
nginx反向代理中server块中的配置信息详解
后端
Sawtone1 小时前
[前端] 面试官:Babel = 比巴卜,想吃了是吧?👿一文速通 Babel 基础知识点,全文干货,干净不废话👋
前端·面试
LH_R1 小时前
OneTerm 开源堡垒机 | 历时三个月重构大更新
运维·后端·安全