【译】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 ↩︎
相关推荐
jjw_zyfx1 小时前
flask before_request 请求拦截器返回无值则放行,有值则拦截
后端·python·flask
hnmpf3 小时前
wtforms+flask_sqlalchemy在flask-admin视图下实现日期的修改与更新
后端·python·flask
江上挽风&sty3 小时前
【Django篇】--创建第一个Django项目
后端·python·django
Gvto3 小时前
使用FakeSMTP创建本地SMTP服务器接收邮件具体实现。
go·smtp·mailtrap
m0_748239334 小时前
随手记录第十四话 -- 在 Spring Boot 3.2.3 中使用 springdoc-openapi-starter-webmvc-ui
spring boot·后端·ui
SomeB1oody4 小时前
【Rust自学】4.2. 所有权规则、内存与分配
开发语言·后端·rust
SomeB1oody4 小时前
【Rust自学】4.5. 切片(Slice)
开发语言·后端·rust
龙少95435 小时前
【Http,Netty,Socket,WebSocket的应用场景和区别】
java·后端·websocket·网络协议·http
计算机学姐5 小时前
基于SpringBoot的校园求职招聘管理系统
java·前端·vue.js·spring boot·后端·mysql·intellij-idea
一起学习计算机5 小时前
29、基于springboot的网上购物商城系统研发
java·spring boot·后端