Go 1.23新特性:Iterators和reflect.Value.Seq
如果你一直在关注 Go 1.23 的新特性,你可能已经听说过新的泛型自定义迭代函数。编写这些迭代器的签名有点复杂,但使用它们很简单。
这里有一个例子:
// counter 从零开始计数
func counter() iter.Seq[int] {
// 暂略过
}
func main() {
for n := range counter() {
if n > 5 {
break
}
fmt.Println(n)
}
}
输出是你所期望的:
0
1
2
3
4
5
如果使用自定义迭代器很简单,那么如何实现它呢?
首先,让我们在 Python 中定义一个类似的函数:
def counter():
n = 0
while True:
yield n
n += 1
在 JavaScript 中:
function* counter() {
let n = 0;
while (true) {
yield n;
n++;
}
}
现在是 Go 版本:
// counter 从零开始计数
func counter() iter.Seq[int] {
return func(yield func(int) bool) {
n := 0
for {
if !yield(n) {
return
}
n++
}
}
}
Go 版本迭代器的有趣之处在于,它没有像 yield
或 function *
这样的特殊语法来创建迭代器函数。类型 iter.Seq
是一个泛型命名类型,表示 func(yield func(V) bool)
,其中 V
是任意类型。换句话说,迭代器只是一个接收回调的普通函数。回调函数使用从迭代值流中获取的值调用,并返回 true
或 false
。如果返回 true
,迭代器应该返回序列中的下一个值。如果返回 false
,迭代器应该进行必要的清理(在这种情况下没有)并停止迭代。
顺便提一下,在 Go 1.24 中,一旦泛型类型别名可用,这可能会写成
return func(yield iter.Yield[int]) {
。旧签名仍然可以使用,但新签名稍微容易些编写和阅读。
你可能已经通过使用 Go 通道编写类似迭代器的东西了。不过基于通道的迭代器有三个主要问题:
- 因为之前 Go 没有泛型,你必须为每种你想要流式传输的类型编写新的辅助程序逻辑。
- 因为它们使用通道和 goroutine,每个发送和接收的值都必须由 Go 运行时调度器调度,这很慢。
- 因为通道应该在一个地方写入或读取,而不是两者都做,你需要到处传递取消通道以通知生成器退出,否则你会遇到很多僵尸 goroutine。
问题1已经解决了。问题2更难解决,但理论上在某些情况下可能可以优化掉一些对运行时调度器的调用。(实际上,这就是一些迭代器现在在底层的工作方式。)但问题3对使用通道作为迭代器的想法确实是致命的。
这是使用退出通道作迭代器的一些样例代码:
func sendOrFail(n int64, out chan<- int64, quit <-chan struct{}) bool {
select {
case out <- n:
return true
case <-quit:
return false
}
}
func counter(quit <-chan struct{}) <-chan int64 {
out := make(chan int64)
go func() {
defer close(out)
var n int64
if ok := sendOrFail(n, out, quit); !ok {
return
}
n++
}()
return out
}
这个方法是在 context.Context
发明之前,所以你可以今天通过使用上下文而不是退出通道来稍微整理一下它,但最终结果是一样的。你需要不断传递一个关闭令牌以知道何时停止迭代,这在嵌套迭代器时变得非常复杂。以下是使用它的样子:
ctx, cancel := context.WithCancel(context.Background())
for n := range counter(ctx.Done()) {
if n > 5 {
break
}
fmt.Println(n)
}
cancel() // 告诉 counter 关闭,否则它会永远泄漏!
实际上,如果你尝试让 sendOrFail
更通用一点,使其接受一个上下文并自动创建和关闭通道,它可能看起来像这样:
func buildChannel[T any](ctx context.Context, callback func(func(T) bool)) <-chan T {
ch := make(chan T)
yield := func(v T) bool {
select {
case ch <- v:
return true
case <-ctx.Done():
return false
}
}
go func() {
defer close(ch)
callback(yield)
}()
return ch
}
func counter(ctx context.Context) <-chan int64 {
return buildChannel(ctx, func(yield func(int64) bool) {
var n int64
for {
if !yield(n) {
return
}
n++
}
})
}
换句话说,你最终得到了与 Go 1.23 迭代器相同的签名,只是多了一个包装函数和上下文参数!
使用 Go 1.23 实际添加的自定义迭代器并编写 return func(yield func(int) bool)
略有点尴尬,但比到处传递关闭令牌并在每个调用点使用它们要好得多。
可以看到 Go 迭代器已经是目前的最佳选择了。
那么reflect.Value.Seq、reflect.Value.Seq2、reflect.Type.CanSeq 和 reflect.Type.CanSeq2 能不能行呢?
reflect.Value.Seq
的文档字符串说:
Seq
返回一个iter.Seq[Value]
,循环遍历 v 的元素。如果 v 的种类是Func
,它必须是一个没有结果并且接受一个类型为func(T) bool
的参数的函数。如果 v 的种类是Pointer
,指针元素类型必须是Array
。否则 v 的种类必须是Int
、Int8
、Int16
、Int32
、Int64
、Uint
、Uint8
、Uint16
、Uint32
、Uint64
、Uintptr
、Array
、Chan
、Map
、Slice
或String
。
调用 Seq
方法就像调用 for x := range v { xValue := reflect.ValueOf(x) }
,适用于任意类型 v
。CanSeq
方法只是报告一个类型是否可以用作迭代器。
可能令人惊讶的是,由于它就像一个 range 语句一样工作,当遍历一个切片时,Seq
方法返回的值只是表示每个切片项索引的整数。
func printEach(x any) {
v := reflect.ValueOf(x)
if !v.Type().CanSeq() {
panic("bad type")
}
for x := range v.Seq() {
fmt.Println(x.Interface())
}
}
func main() {
printEach([]string{"a", "b", "c"})
}
Output:
0
1
2
要获取切片元素,请使用 Seq2
和第二个 range 值:
func printEach(x any) {
v := reflect.ValueOf(x)
if !v.Type().CanSeq2() {
panic("bad type")
}
for x, y := range v.Seq2() {
fmt.Println(x.Interface(), y.Interface())
}
}
func main() {
printEach([]string{"a", "b", "c"})
}
输出:
0 a
1 b
2 c
反射包已经有一个方法可以遍历 map,即 reflect.Value.MapRange
,但使用它不如使用 reflect.Value.Seq2
那么好:
iter := reflect.ValueOf(m).MapRange()
for iter.Next() {
k := iter.Key()
v := iter.Value()
// ...
}
// vs.
for k, v := range reflect.ValueOf(m).Seq2() {
// ...
}
原文:https://blog.carlana.net/post/2024/golang-reflect-value-seq/