Go 1.23 新特性 Range-Over Functions
介绍
在Go 1.23中,添加了一项新的实验性功能:range-over function。这一特性为Go语言引入了类似于迭代器的标准协议,简化了在容器类型上的迭代操作。本文将通过代码示例,详细讲解这一新特性。
基本迭代模式
首先,让我们回顾一下在没有range-over function时,如何对容器类型进行迭代。以下是一个基于链表实现的简单栈(Stack)类型的例子:
type node[T any] struct {
value T
next *node[T]
}
type Stack[T any] struct {
head *node[T]
}
func (s *Stack[T]) Push(v T) {
s.head = &node[T]{v, s.head}
}
var ErrEmpty = errors.New("empty stack")
func (s *Stack[T]) Pop() (T, error) {
if s.head == nil {
var v T
return v, ErrEmpty
}
n := s.head
s.head = s.head.next
return n.value, nil
}
以上代码定义了一个栈结构,并提供了Push和Pop方法。栈并不直接跟踪任何迭代状态,因此我们需要额外实现一个迭代器来完成单次迭代的支持。
func (s *Stack[T]) Items() *StackIterator[T] {
return &StackIterator[T]{s.head}
}
type StackIterator[T any] struct {
node *node[T]
}
func (s *StackIterator[T]) Next() (T, bool) {
if s.node == nil {
var v T
return v, false
}
n := s.node
s.node = s.node.next
return n.value, true
}
以上代码实现了一个迭代器来遍历栈。使用方式如下:
it := s.Items()
for v, ok := it.Next(); ok; v, ok = it.Next() {
fmt.Println(v)
}
虽然这种方式可以工作,但它在编写与容器解耦的迭代逻辑时仍显繁琐。因此,我们可以引入一种更通用的方式来实现控制反转的迭代。
控制反转
我们可以通过引入控制反转,编写一个通用的Do方法,让用户传递逻辑处理函数来处理栈中的每一个元素。如下所示:
func (s *Stack[T]) Do(yield func(v T)) {
for n := s.head; n != nil; n = n.next {
yield(n.value)
}
}
使用方式如下:
s.Do(func(n int) {
fmt.Println(n)
})
Do方法允许用户定义具体的逻辑(如打印、保存到文件等),并将此逻辑应用于栈的每一个元素。然而,该方法的一个局限性是无法中途停止迭代。为了克服这一问题,Go 1.22引入了range-over function。
Range-Over Functions
使用range-over function,你可以通过迭代器函数和for-range循环轻松实现对容器的迭代,甚至可以控制迭代的停止。我们首先定义一个新方法来支持range-over function:
func (s *Stack[T]) Iter() func(func(T) bool) {
iter := func(yield func(T) bool) {
for n := s.head; n != nil; n = n.next {
if !yield(n.value) {
return
}
}
}
return iter
}
以上方法返回一个符合iter.Seq类型的函数,用户可以使用它进行迭代:
for v := range s.Iter() {
fmt.Println(v)
}
此外,我们还可以定义返回键值对的迭代器:
func (s *Stack[T]) Iter2() func(func(int, T) bool) {
iter := func(yield func(int, T) bool) {
for i, n := 0, s.head; n != nil; i, n = i+1, n.next {
if !yield(i, n.value) {
return
}
}
}
return iter
}
使用方式如下:
for i, v := range s.Iter2() {
fmt.Println(i, v)
}
使用Pull函数提取值
iter包中还提供了Pull函数,用于逐个提取迭代值并控制迭代停止。以下是一个计算栈中最大值的例子:
func Max[T cmp.Ordered](seq iter.Seq[T]) (T, error) {
pull, stop := iter.Pull(seq)
defer stop()
max, ok := pull()
if !ok {
return max, fmt.Errorf("Max of empty sequence")
}
for v, ok := pull(); ok; v, ok = pull() {
if v > max {
max = v
}
}
return max, nil
}
使用方式如下:
m, err := Max(s.Iter())
if err != nil {
fmt.Println("ERROR:", err)
} else {
fmt.Println("max:", m)
}
结论
range-over function为Go语言提供了一种通用的迭代器机制,通过iter.Seq和iter.Seq2,可以使用熟悉的for循环进行迭代,并让实现库的开发者承担迭代的具体实现。我希望通过这些示例,帮助你理解这一新特性并在实际开发中加以应用。