Go 1.23 迭代器,统一标准,改善 Go 生态系统

2024-09-13 02:58:24 浏览数 (1)

前言

Go 1.23 版本在北京时间 2024814 日凌晨 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 中,迭代器 实际上是指符合以下三种函数签名之一的函数:

代码语言:go复制
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 包,该包定义了两种迭代器类型,分别是 SeqSeq2,用于处理不同的迭代场景。

代码语言:go复制
package iter

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

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

SeqSeq2 的区别:

  • 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 提供的迭代器机制来实现这个功能。

代码语言:go复制
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复制
程序员

标准库新增的迭代器函数

随着迭代器的引入,slicesmaps 包也新增了一些与迭代器一起使用的函数。

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 包的功能扩展,以及 slicesmaps 标准库中新增的与迭代器相关的函数。

有人认为,引入迭代器使 Go 变得更加复杂,因为迭代器的代码实现可能会影响可读性。对于刚接触 Go 迭代器的开发者来说,确实可能感到有些不适应。不过,Go 官方为了简化迭代器的使用,新增了 iter 包,并在 slicesmaps 包中提供了许多便捷函数,以提升开发体验。

总的来说,引入 标准迭代器 统一了迭代器的设计和使用方式,解决了各自为政的问题,进一步优化了 Go 的生态系统。

你对迭代器有什么看法?欢迎在评论区留言探讨。

参考资料

Range Over Function Types

推荐阅读

Go 1.23 版本发布啦,这些重大更新你一定要知道!

Go 1.23 新特性:Timer 和 Ticker 的重要优化

Go 1.23 新特性:slices 和 sync 等核心库的微调,大幅提升开发体验

你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。

成功的路上并不拥挤,有没有兴趣结个伴?

关注我,加我好友,一起学习一起进步!

0 人点赞