【译】Go 迭代器的乐趣

原文链接:Fun with Go Iterators | xnacly - blog 作者:[Matteo](xnacly - blog)

文章介绍了Go语言1.23版本引入的迭代器支持,并通过自定义封装实现链式操作,简化了迭代过程。作者分享了如何使用新的迭代器包和自定义结构来实现流畅的链式操作,提供了示例代码和实现细节,展示了Go语言在迭代操作上的新特性。

目录:

  • 烦恼
  • 解决方案(看似)
  • 实现逻辑
    • Each
    • Reverse
    • Map
    • Filter
  • 示例与思考

Go 1.23 版新增了迭代器支持^1^iter 软件包^2^。现在,我们可以对常量、容器(映射、切片、数组、字符串)和函数进行循环。起初,我觉得迭代器的创建很笨拙,而迭代器的使用似乎很简单。

迭代器的在 "go 语言"方式给我带来的问题是,你无法像在 JavaScript 中那样对它们进行链式处理:

js 复制代码
[1,2,3,4]
    .reverse()
    .map(e => e*e)
    .filter(e => e % 2 == 0)
    .forEach(e => console.log(e)) 

烦恼

如果用 go 语言编写,我们需要调用 5 个函数:

go 复制代码
slices.ForEach(
    slices.Filter(
        slices.Map(
            slices.Reverse(slices.All([]int{1,2,3,4})),
            func(i int) int { return i * i},
        ),
        func(i int) bool { return i % 2 == 0 }
    ),
    func(i int) { fmt.Println(i) }
}

这是一个示例,切片 slices 软件包^3^中没有 Map、Filter 或 ForEach 函数

解决方案(看似)

因为我非常不喜欢写这样的链式 "函数式" 操作,就像 Python 一样(别来烦我,Haskell 语言的兄弟们)--我想使用新的迭代器和 iter 包,并用一个结构将其封装起来,以便实现 JavaScript 那种简洁而流畅的链式操作。

下面是 Go 中使用相同的操作,但我没有使用 iterslices 包,而是使用了抽象:

go 复制代码
func TestIterator(t *testing.T) {
    From([]int{1, 2, 3, 4}).
	Reverse().
	Map(func(i int) int { return i * i }).
	Filter(func(i int) bool { return i%2 == 0 }).
	Each(func(a int) { println(a) })
    // 16
    // 4
}

实现逻辑

让我们来看看它的实现,让我来介绍一下迭代器结构(Iterator struct)。它封装了迭代器 (*Iterator).iter ,因此允许我在此结构上调用函数,而不必将每个迭代器函数作为下一个函数的参数。

go 复制代码
type Iterator[V any] struct {
    iter iter.Seq[V]
}

让我们来看看在谈到迭代器时,我们首先想到的函数:从片段中创建一个迭代器,以及将一个迭代器集合到片段中:

go 复制代码
func (i Iterator[V]) Collect() []V {
	collect := make([]V, 0)
	for e := range i.iter {
		collect = append(collect, e)
	}
	return collect
}

func From[V any](slice []V) *Iterator[V] {
	return &Iterator[V]{
		iter: func(yield func(V) bool) {
			for _, v := range slice {
				if !yield(v) {
					return
				}
			}
		},
	}
}

第一个函数尽可能直截了当--创建片段、循环迭代器、添加每一个元素、返回切片。第二个函数突出显示了 go 中创建迭代器的奇怪方式。让我们先看一下签名,我们返回的是指向结构体的指针,这样被调用者就可以调用所有方法,而不必为每个方法使用临时变量。在函数本身中,迭代器是通过返回一个闭包创建的,该闭包循环遍历参数并返回,当 yield 函数返回 false 时,迭代器停止。

Each

ForEach / Each 方法是我想要的下一个函数,它只需针对迭代器中的每个元素执行传入的函数。

go 复制代码
func (i *Iterator[V]) Each(f func(V)) {
	for i := range i.iter {
		f(i)
	}
}

可以这样使用:

go 复制代码
From([]int{1, 2, 3, 4}).Each(func(a int) { println(a) })
// 1
// 2
// 3
// 4

Reverse

要反转迭代器,我们首先需要收集所有元素,然后从收集到的片段中构建一个新的迭代器,幸运的是,我们有专门用于此的函数:

go 复制代码
func (i *Iterator[V]) Reverse() *Iterator[V] {
	collect := i.Collect()
	counter := len(collect) - 1
	for e := range i.iter {
		collect[counter] = e
		counter--
	}
	return From(collect)
}

同样,这对反转切片也很有用:

go 复制代码
From([]int{1, 2, 3, 4}).Reverse().Each(func(a int) { println(a) })
// 4
// 3
// 2
// 1

Map

对迭代器的每个元素进行转变也是必要的:

go 复制代码
func (i *Iterator[V]) Map(f func(V) V) *Iterator[V] {
	cpy := i.iter
	i.iter = func(yield func(V) bool) {
		for v := range cpy {
			v = f(v)
			if !yield(v) {
				return
			}
		}
	}
	return i
}

首先,我们复制前一个迭代器,这样做可以避免在迭代器中引用 i.iter 迭代器而导致堆栈溢出。Map 的工作原理是用一个新的迭代器覆盖 i.iter,该迭代器消费 cpy 迭代器的每个字段,并用 v 传递给 f 的结果覆盖迭代器值,从而映射迭代器。

Filter

在映射之后,使用最多的流式/函数式 api 方法可能就是 "Filter "了。让我们来看看我们的最终操作:

go 复制代码
func (i *Iterator[V]) Filter(f func(V) bool) *Iterator[V] {
	cpy := i.iter
	i.iter = func(yield func(V) bool) {
		for v := range cpy {
			if f(v) {
				if !yield(v) {
					return
				}
			}
		}
	}
	return i
}

与 Map 类似,我们消费复制的迭代器,并以 v 作为参数调用 f,如果 f 返回 true,我们就将其保留在新的迭代器中。

示例与思考

切片和 iter 软件包与 go 1.18^4^ 中引入的通用系统配合得非常好。

虽然这种 API 更容易使用,但我理解 go 团队不这样实现迭代器的原因。下面是一些测试示例和运行结果。

go 复制代码
package iter1

import (
	"fmt"
	"testing"
	"unicode"
)

func TestIteratorNumbers(t *testing.T) {
	From([]int{1, 2, 3, 4}).
		Reverse().
		Map(func(i int) int { return i * i }).
		Filter(func(i int) bool { return i%2 == 0 }).
		Each(func(a int) { println(a) })
}

func TestIteratorRunes(t *testing.T) {
	r := From([]rune("Hello World!")).
		Reverse().
		// remove all spaces
		Filter(func(r rune) bool { return !unicode.IsSpace(r) }).
		// convert every rune to uppercase
		Map(func(r rune) rune { return unicode.ToUpper(r) }).
		Collect()
	fmt.Println(string(r))
}

func TestIteratorStructs(t *testing.T) {
	type User struct {
		Id   int
		Name string
		Hash int
	}

	u := []User{
		{0, "xnacly", 0},
		{1, "hans", 0},
		{2, "gedigedagedeio", 0},
	}

	From(u).
		// computing the hash for each user
		Map(func(u User) User {
			h := 0
			for i, r := range u.Name {
				h += int(r)*31 ^ (len(u.Name) - i - 1)
			}
			u.Hash = h
			return u
		}).
		Each(func(u User) { fmt.Printf("%#+v\n", u) })
}

运行这些程序的结果是:

css 复制代码
$ go test ./... -v
=== RUN   TestIteratorNumbers
16
4
--- PASS: TestIteratorNumbers (0.00s)
=== RUN   TestIteratorRunes
!DLROWOLLEH
--- PASS: TestIteratorRunes (0.00s)
=== RUN   TestIteratorStructs
&iter1.User{Id:0, Name:"xnacly", Hash:20314}
&iter1.User{Id:1, Name:"hans", Hash:13208}
&iter1.User{Id:2, Name:"gedigedagedeio", Hash:44336}
--- PASS: TestIteratorStructs (0.00s)
PASS
ok      iter1   0.263s

就是这样,一个围绕 iterslices 的封装器,它反映了 JavaScript 提供的 streaming 流,只不过是在 go 中。

参考文档:

  1. go.dev/doc/go1.23#... ↩︎
  2. go.dev/doc/go1.23#... ↩︎
  3. pkg.go.dev/slices ↩︎
  4. go.dev/doc/go1.18 ↩︎
相关推荐
2401_8581202622 分钟前
医院管理新境界:Spring Boot技术突破
java·spring boot·后端
编程、小哥哥1 小时前
netty之SpringBoot+Netty+Elasticsearch收集日志信息数据存储
spring boot·后端·elasticsearch
ᴡᴀᴋᴜ⌓‿⌓ᴡᴀᴋᴜ2 小时前
手动在Linux服务器上部署并运行SpringBoot项目(新手向)
linux·服务器·spring boot·后端
raiseup24 小时前
尚硅谷rabbitmq2024 集群搭建和优先队列答疑 第41-48节
开发语言·后端·ruby
江南一点雨4 小时前
Spring Boot3集成 LiteFlow 实现业务流程编排
java·spring boot·后端
荔枝爱编程5 小时前
《小白:深入解析 Spring 事务与 MySQL 事务》
java·后端·mysql
karlhong_weihao5 小时前
手写Spring第三篇番外,反射的基本使用
java·后端·spring
涛粒子6 小时前
SpringBoot 集成 Redis
spring boot·redis·后端
fenglllle6 小时前
springboot kafka多数据源,通过配置动态加载发送者和消费者
spring boot·后端·kafka