Go 1.21 中的泛型推断

原文: understanding Go 1.21 generics type inference

Go 1.21 已经发布了,带来了一系列的改进,例如更好的泛型类型推断(本文的内容);新的内置函数min`,maxclear`;以及标准库中的几个新软件包(maps`,slices`,cmp`,log/slogtesting/slogtest`)。在这里阅读完整的发行说明。

至少对我们Encore来说,特别感兴趣的是对泛型类型推断的改进,因为它会影响Encore的静态分析的工作方式。但是,我们认为发行说明很难理解,因此本文通过更多解释和示例介绍这次的变更。

我们也刚刚发布了支持 Go 1.21 的 Encore v1.24,所以这是尝试这些新变化的好方法。

部分实例化的泛型函数

来自 Go 1.21 发行说明:

现在可以使用本身(可能部分实例化)泛型函数的参数调用(可能部分实例化的泛型)函数。编译器将尝试推断被调用方缺少的类型参数(as before),对于每个未完全实例化的泛型函数的参数,将尝试推断其缺少的类型参数(new)。典型的用例是对在容器上运行的泛型函数的调用(例如slices.IndexFunc),其中函数参数也可以是泛型的,并且被调用函数的类型参数及其参数是从容器类型推断出来的。

啥意思?

考虑一个函数,该函数需要传入一个值并报告它是否为零值:。

1
func IsZero[T comparable](a T) bool

在 Go 1.21 之前,将这样的函数作为参数传递给另一个(泛型或非泛型)函数需要你显式指定类型参数,即使从上下文中很明显能看出类型。
例如:

1
2
3
4
5
6
7
8
9
// Any reports whether fn returns true for any number in the slice.
func Any(numbers []int, fn func(int) bool) bool {
for i, v := range numbers {
if fn(v) {
return true
}
}
return false
}

fn处理numbers,只要有一个是true,Any就返回true

使用Go 1.21,您现在就可以这些写:

1
2
3
4
5
6
7
numbers := []int{1, 2, 3}
// Go 1.21
anyZeroes := Any(numbers, IsZero)
// Go 1.21之前,你不得不这样写
anyZeroes := Any(numbers, IsZero[int])

如发行说明所示,这也适用于泛型函数。所以你也可以写:

1
2
3
4
5
// Go 1.21
firstZeroIndex := slices.IndexFunc(numbers, IsZero)
// Go 1.21之前,你不得不这样写
firstZeroIndex := slices.IndexFunc(numbers, IsZero[int])

Go 1.21可以从类型numbers推断IsZeroIsZero[int] ,即使slices.IndexFunc是一个泛型函数。

发行说明中在同一段落继续讲到:

1
更一般地说,如果可以从赋值推断类型参数,则在将泛型函数分配给变量或作为结果值返回时,现在可以在没有显式实例化的情况下使用泛型函数。

这意味着以下内容现在有效(在 Go 1.21 之前,您需要显式指定类型参数):

1
2
3
4
5
6
7
// IsZero 被推断为 IsZero[string],因为根据类型func(a string) bool可以推断出
var isZeroString func(a string) bool = IsZero
func IsNilPointerFactory[T any] func() func(val *T) bool {
// IsZero 是 IsZero[*T], 因为根据返回值func(val *T) bool可以推断出来
return IsZero
}

接口赋值推断

Go 1.21 还改进了泛型接口类型的类型推断。从发行说明:

类型推断现在还会在将值分配给接口时考虑方法:方法签名中使用的类型参数的类型参数可以从匹配方法的相应参数类型推断出来。

这是啥意思呢?请考虑以下泛型接口:

1
2
3
4
type RandomElementer[T any] interface {
// 返回一个随机的元素,如果集合为空,返回(zero, false)
RandomElement() (T, bool)
}

然后考虑一个泛型的帮助程序函数,该函数调用某个集合,但如果集合为空,则会panic:

1
2
3
4
5
6
7
8
// 必须返回一个元素,如果集合为空,则panic
func MustRandom[T any](collection RandomElementer[T]) T {
val, ok := collection.RandomElement()
if !ok {
panic("collection is empty")
}
return val
}

在 Go 1.21 中,现在可以调用接口类型,并且接口类型将从参数中推断出来。考虑以下两种类型,一种泛型的,一种不是:

1
2
3
4
5
6
7
// MyList 泛型集合.
type MyList[T any] []T
func (l MyList[T]) RandomElement() (T, bool) { /* ... */ }
// IPSet 代表一个IP集合,非泛型的
type IPSet map[netip.Addr]bool
func (s IPSet) RandomElement() (netip.Addr, bool) { /* ... */ }

这些现在(Go1.21)可以像这样使用:

1
2
3
4
5
6
randomInt := MustRandom(MyList[int]{})
randomIP := MustRandom(IPSet{})
// Go 1.20或者以前,你不得不写成如下的方式
randomInt := MustRandom[int](MyList[int]{})
randomIP := MustRandom[netip.Addr](IPSet{})

优秀!继续前进:

1
同样,由于类型参数必须实现其相应约束的所有方法,因此类型参数和约束的方法匹配,这可能导致推断其他类型参数。

这基本上意味着上述类型推断也扩展到采用额外类型的函数。 例如,MustRandom签名可以重写为:

1
func MustRandom[R RandomElementer[T], T any](collection R) T

当调用MustRandom(IPSet{})时,Go 1.21 在调用时正确推断RT

非类型化常量的类型推断

如果将多个不同类型的非类型化常量参数(如非类型化 int 和非类型化浮点常量)传递给具有相同(未另行指定)类型参数类型的参数,这不会报错,类型推断现在使用与具有非类型化常量操作数的运算符相同的方法确定类型。此更改使从非类型常量参数推断的类型与常量表达式的类型一致

Go 具有非类型常量的概念,并且表达式的类型a+b是从常量值ab推断出来的:

1
2
var x = 1 + 2 // x 是int
var y = 1 + 2.5 // y 是 float64

现在考虑编写一个行为类似于内置运算符+的泛型函数Add:

1
2
3
func Add[T int | float64](a, b T) T {
return a + b
}

在 Go 1.21 中,这与您所期望的完全一样工作:

1
2
var x = Add(1, 2) // x 是 int
var y = Add(1, 2.5) // y 是 float64

在 Go 1.20 中,相同的代码无法编译,因为类型推断将分别考虑每个参数。它将推断 1的类型是int, 然后无法与2.5的类型float64统一,从而导致错误“default type float64 of 2.5 does not match inferred type int for T”。

总结

这就是 Go 1.21 中对泛型类型推断的更改程度。希望这些示例有助于使更改更易于理解;反正经过摸索我更容易理解了,家人们如果理解了请在页面一键三连。