原文链接: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 中使用相同的操作,但我没有使用 iter
和 slices
包,而是使用了抽象:
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
就是这样,一个围绕 iter
和 slices
的封装器,它反映了 JavaScript 提供的 streaming 流,只不过是在 go 中。
参考文档: