在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的工程化思想。