Go 1.23 迭代器介绍

前言

Go 1.23 版本在北京时间 2024 年 8 月 14 日凌晨 1:03 发布,该版本带来了多项重大更新,本文将重点介绍 iterator 标准迭代器。

为什么引入标准迭代器

迭代器在 Go 语言中并非新概念,实际上,它一直存在于 Go 的生态系统中。如果你经常使用 Go 标准库,可能已经注意到某些库提供了迭代器的实现,例如:bufio.Scannerdatabase.Rowsfilepath.Walk(Dir)flag.Visitsync.Map.Range 等。那么为什么 Go 官方仍然会提供统一的迭代器标准呢?主要原因在于现有的迭代器设计和使用方式各不相同。当我们使用一个新库的迭代器时,通常需要学习它的具体使用方法。如果能够统一迭代器的标准化形式,我们只需掌握标准迭代器的定义和使用方式,便可以适应所有迭代器。

迭代器

Go 1.23 中,迭代器 实际上是指符合以下三种函数签名之一的函数:

func(yield func() bool)
func(yield func(V) bool)
func(yield func(K, V) bool)

如果一个函数或方法返回的值符合上述形式之一,那么该返回值就可以被称为 迭代器

迭代器又分为 推迭代器拉迭代器,上面这种设计是典型的 推迭代器,通过调用 yield 函数逐步推出一系列值,yield 函数返回 bool,决定是否继续执行推出操作。

代码示例:

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 开始,新增了对函数类型的支持。不过,函数类型必须是前面所提到的三种类型之一,也就是函数需要实现迭代器。

代码示例:

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
			}
		}
	}
}

程序运行结果:

1 陈明勇
0 程序员

iter 包

为了简化迭代器的使用,Go 1.23 版本新增了一个 iter 包,该包定义了两种迭代器类型,分别是 SeqSeq2,用于处理不同的迭代场景。

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]

    • Seq 是一个泛型类型的函数,接收一个 yield 函数作为参数。它推出单个元素,例如切片的索引或映射中的键。yield 函数返回 bool,决定是否继续迭代。
    • 使用场景:可以用于返回一个单值的迭代,比如切片中的索引或值,或映射中的键或值。
  • Seq2[K, V any]

    • Seq2 是一个泛型类型的函数,接收一个 yield 函数,推送一对元素,例如切片中的索引和值,或者映射中的键值对。yield 函数同样返回 bool,以决定是否继续迭代。
    • 使用场景:当需要同时返回两个值(如键和值)时使用。

在 Set 集合中使用迭代器的案例

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
			}
		}
	}
}

完整的代码案例:

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)
	}
}

程序运行结果:

程序员
陈明勇

拉迭代器

在讲 拉迭代器 之前,我们先了解一下 推拉迭代器 两者的区别:

  • 推迭代器 将容器中的每个值主动推送到 yield 函数中。在 Go 语言中,我们可以通过 for-range 循环直接接收被推送的值。

  • 与此相反,拉迭代器 则是由调用方主动请求数据。每次调用拉迭代器时,它从容器中拉出下一个值并返回。虽然 for/range 语句不直接支持拉迭代器,但通过普通的 for 循环可以轻松实现对拉迭代器的迭代。

值得高兴的是,我们并不需要手动实现一个拉迭代器,因为 iter 包提供了 Pull 函数,该函数接收一个 标准(推)迭代器 类型的参数,返回两个参数,第一个参数是 拉迭代器,第二个参数是 停止 函数。当我们不再需要拉取元素的时候,调用 停止 函数即可。

代码示例:

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,表示迭代结束,无法再继续获取元素。

程序运行结果:

程序员

标准库新增的迭代器函数

随着迭代器的引入,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 的生态系统。

posted @   燕山督云侠  阅读(104)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示