【译】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 ↩︎
相关推荐
爱敲代码的小冰6 分钟前
spring boot 请求
java·spring boot·后端
java小吕布1 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy2 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄2 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫2 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____2 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng3 小时前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss3 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
大鲤余3 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust
她说彩礼65万4 小时前
Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
后端·asp.net·mvc