函数类型的 Range - Go 编程语言
简介
Go 1.23 引入了一个新特性:函数类型的 range。本文将详细介绍为什么要添加这个特性,它是什么,以及如何使用它。
为什么要添加这个特性?
自 Go 1.18 以来,Go 语言已经支持编写新的泛型容器类型。例如,我们可以创建一个基于 map
实现的泛型 Set
类型:
type Set[E comparable] struct {
m map[E]struct{}
}
该 Set
类型可以提供方法来添加元素和检查元素是否存在:
func (s *Set[E]) Add(v E) {
s.m[v] = struct{}{}
}
func (s *Set[E]) Contains(v E) bool {
_, ok := s.m[v]
return ok
}
然而,遍历集合元素是很常见的需求。我们希望为用户提供一种标准化的方式来遍历 Set
中的元素。
推送元素的遍历方式
我们可以为 Set
添加一个方法,接受一个函数作为参数,并将每个元素推送给该函数。我们称这个方法为 Push
:
func (s *Set[E]) Push(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
break
}
}
}
用户可以使用 Push
方法打印 Set
中的所有元素:
s.Push(func(v string) bool {
fmt.Println(v)
return true
})
拉取元素的遍历方式
另一种方法是返回一个函数,每次调用该函数都会返回集合中的下一个值。我们可以称这个方法为 Pull
,并实现如下:
func (s *Set[E]) Pull() (next func() (E, bool), stop func()) {
ch := make(chan E)
done := make(chan struct{})
go func() {
defer close(ch)
for v := range s.m {
select {
case ch <- v:
case <-done:
return
}
}
}()
return func() (E, bool) {
v, ok := <-ch
return v, ok
},
func() {
close(done)
for range ch {
}
}
}
我们可以这样使用 Pull
方法:
next, stop := s.Pull()
defer stop()
for {
v, ok := next()
if !ok {
break
}
fmt.Println(v)
}
Go 1.23 的改进
Go 1.23 为 for/range
语句添加了对用户定义容器类型的支持,并引入了标准化的迭代器。新的 range
支持函数类型,只要这些函数符合特定的签名。
我们可以使用函数类型 range
来遍历自定义容器,比如我们的 Set
。假设我们为 Set
定义一个 All
方法返回迭代器:
func (s *Set[E]) All() iter.Seq[E] {
return func(yield func(E) bool) {
for v := range s.m {
if !yield(v) {
return
}
}
}
}
这样,我们可以通过 for/range
轻松遍历 Set
中的所有元素:
for v := range s.All() {
fmt.Println(v)
}
结语
Go 1.23 的函数类型 range
提供了一种更标准化、更易用的方式来遍历自定义容器。通过这个新特性,开发者可以更轻松地构建泛型容器,并以更简洁的方式实现容器元素的遍历。