在Go1.18版本时,Go新增了泛型支持,开发者可以不限于内置数据类型(map slice chanel string),来自定义泛型数据类型。
但在Go1.22之前,range关键字只能用来遍历map slice chanel string,直到Go1.22版本支持了int类型。
在Go1.23版本更新后,range加入了三种函数类型的支持,分别为
1func(yield func() bool)
2func(yield func(K) bool)
3func(yield func(K, V) bool)
其中两种已经被定义在标准库iter中。
range现在可以遍历这三种函数类型,这三种函数分别接收一个0-2个参数并返回bool的函数类型参数,与之对应的range形式为
1for range func(yield func() bool) { _ = yield } {
2 //do something
3}
4for k := range func(yield func(k int) bool) { _ = yield } {
5 _ = k
6 //do something
7}
8for k, v := range func(yield func(k, v int) bool) { _ = yield } {
9 _ = k
10 _ = v
11 //do something
12}
上面的代码在Go1.23中可以顺利通过编译,你可能会很困惑,这样做有什么用?
yield函数
1func iter2(yield func(int, int) bool) {
2 for i := range 2 {
3 if !yield(i, i+1) {
4 return
5 }
6 }
7}
8
9func main() {
10 for k, v := range iter2 {
11 fmt.Println("iter2", k, v)
12 }
13}
执行上面的代码,输出为
1iter2 0 1
2iter2 1 2
我们可以发现,在iter2内调用yield函数传入的两个参数被我们写的fmt.Println(“iter2”, k, v)打印出来了。 如果我们不使用range,想要实现同样的效果也是可以的,像这样
1func iter2(yield func(int, int) bool) {
2 for i := range 2 {
3 if !yield(i, i+1) {
4 return
5 }
6 }
7}
8
9func main() {
10 iter2(func(k, v int) bool {
11 fmt.Println("iter2", k, v)
12 return true
13 })
14}
但是这样看起来远不如使用range清晰易读,实际上这也是Go编译器在做的事(类似语法糖),当在range内使用break时,编译生成的yield函数会对应添加return false,continue则会添加return true,如果既不break也不continue,也会在末尾加入一个return true,就像上面一样。例如
1func iter5(yield func(int, int) bool) {
2 for i := range 5 {
3 if !yield(i, i+1) {
4 return
5 }
6 }
7}
8
9func main() {
10 iter5(func(k, v int) bool {
11 fmt.Println("iter2", k, v)
12 if k == 2 {
13 return false
14 }
15 return true
16 })
17
18 // 等同于
19 for k, v := range iter5 {
20 fmt.Println("iter2", k, v)
21 if k == 2 {
22 break
23 }
24
25 }
26}
仔细看,上面的iter5函数中是我们手动去判断yield函数的返回值来实现range内部continue和break的效果的,如果我不判断呢?
1func iter5(yield func(int, int) bool) {
2 for i := range 5 {
3 yield(i, i+1)
4 }
5}
6
7func main() {
8 iter5(func(k, v int) bool {
9 fmt.Println("iter2", k, v)
10 if k == 2 {
11 return false
12 }
13 return true
14 })
15
16 // 等同于
17 for k, v := range iter5 {
18 fmt.Println("iter2", k, v)
19 if k == 2 {
20 break
21 }
22
23 }
24}
输出为
1iter2 0 1
2iter2 1 2
3iter2 2 3
4iter2 3 4
5iter2 4 5
6iter2 0 1
7iter2 1 2
8iter2 2 3
9panic: runtime error: range function continued iteration after function for loop body returned false
10
11goroutine 1 [running]:
12main.main-range1(...)
13 /tmp/sandbox542296131/prog.go:23
14main.iter5(...)
15 /tmp/sandbox542296131/prog.go:9
16main.main()
17 /tmp/sandbox542296131/prog.go:23 +0x1b3
可见Go编译器并不是简单的将函数编译处理,同时也加入了运行时检查,如果不去检查yield返回值,会直接panic掉。
用法
为我们自定义的Set类型实现迭代器而无需对外暴露内部实现
1package main
2
3import (
4 "fmt"
5 "iter"
6)
7
8// Set holds a set of elements.
9type Set[E comparable] struct {
10 m map[E]struct{}
11}
12
13// New returns a new [Set].
14func New[E comparable]() *Set[E] {
15 return &Set[E]{m: make(map[E]struct{})}
16}
17
18// Add adds an element to a set.
19func (s *Set[E]) Add(v E) {
20 s.m[v] = struct{}{}
21}
22
23func (s *Set[E]) All() iter.Seq[E] {
24 return func(yeild func(e E) bool) {
25 for k := range s.m {
26 if !yeild(k) {
27 return
28 }
29 }
30 }
31}
32
33func main() {
34 set := New[string]()
35 set.Add("hello")
36 set.Add("hello")
37 set.Add("world")
38
39 for v := range set.All() {
40 fmt.Println(v)
41 }
42}
更优雅遍历Scanner
1var pr *io.PipeReader
2for chunk, err := range ScannerToken(logReader) {
3 if err != nil {
4 slog.Error("log reader error", "error", err)
5 break
6 }
7 fmt.Println(chunk)
8}
9
10func ScannerToken(reader *bufio.Scanner) iter.Seq2[string, error] {
11 return func(yield func(string, error) bool) {
12 for reader.Scan() {
13 chunk := reader.Text()
14 if chunk == "" {
15 continue
16 }
17 if !yield(chunk, nil) {
18 return
19 }
20 }
21 yield("", reader.Err())
22 }
23}
遍历消息列表,可以实现指定类型遍历,源码见我写的nsxbot
1type CommonMessage struct {
2 SubType string `json:"sub_type"`
3 MessageId int `json:"message_id"`
4 UserId int64 `json:"user_id"`
5 Messages []schema.Message `json:"message"`
6 RawMessage string `json:"raw_message"`
7 Font int `json:"font"`
8 Sender schema.Sender `json:"sender"`
9}
10
11// yield the rawMessage by type
12func (cm CommonMessage) FilterType(Type string) iter.Seq[json.RawMessage] {
13 return func(yield func(json.RawMessage) bool) {
14 for _, msg := range cm.Messages {
15 if msg.Type == Type {
16 if !yield(msg.Data) {
17 return
18 }
19 }
20 }
21 }
22}
23
24// yield all messages use type and raw
25func (cm CommonMessage) All() iter.Seq2[string, json.RawMessage] {
26 return func(yield func(string, json.RawMessage) bool) {
27 for _, msg := range cm.Messages {
28 if !yield(msg.Type, msg.Data) {
29 return
30 }
31 }
32 }
33}
总结
Go官方新增的这种标准化迭代器设计,未来一定会规范各种第三方库数据类型的迭代方式,避免了各自为政,十分符合Go的工程化思想。