Rob 反对修改 Go 1.18 泛型标准库
涛叔Go 语言之父 Rob Pike 昨天创建 issue,反对在 Go 1.18 的标准库中引入泛型支持。Rob 此举,既在意料之外,又在情理之中。Rob 的依据有两点:一个是改动范围过大,再一个是缺乏实战经验。所以 Rob 建议先不要在 Go 1.18 修改标准库,而是先在 golang.org/x 或者 golang.org/exp 提供相关库的泛型版本。经过一到两个版本的实际使用,才能积累足够的经验,届时再考虑更新标准库。那社区原本要对标准库做哪些有争议改动呢?请容我细细道来。
泛型语法简介
在开始之前,先简单介绍一下 Go 语言泛型的语法。比如要实现一个简单的Max
函数,可以这样写:
func Max[T any](a, b T) T {
if a < b {
return b
}
return a
}
这里需要在函数名Max
和参数列表(a,b)
之间插入[T any]
。其中的T
就是泛型参数,后面的any
表示泛型参数的限制范围(constraint)。我们后面会细说,大家知道 Go 语言要求泛型参数必须指定范围。声明泛型参数后,就可以在参数列表、返回值列表和函数内部使用泛型参数了。
调用泛型函数的时候需要指明实际的类型:
.Println(Max[int](1, 2)) // 2
fmt.Println(Max[float32](1.1, 2.2)) // 2.2
fmt.Println(Max[string]("a", "b")) // b fmt
为了方便使用,Go 泛型还支持类型推导。也就是说,Go 可以根据函数参数的类型来推断泛型参数的类型,所以上述例子可以改写成:
.Println(Max(1, 2)) // 2
fmt.Println(Max(1.1, 2.2)) // 2.2
fmt.Println(Max("a", "b")) // b fmt
是不是看起来跟普通函数没有区别了!
这里有一个问题,我们可以执行下面的调用吗?
.Println(Max([]int{1}, []int{2})) fmt
答案是不能。因为在Max
函数中有if a < b {}
这样的比较判断,而 slice 是不能相互比较的(具体见comparable)。也就是说,我们虽然想用T
表示不同的参数类型,但Max
的实现决定了只能传入可以相互比较的类型。这就是限制范围(constraint)需要解决的问题。
我们前面说过,泛型参数都需要指定限制范围。那如何定义 constraint 呢?这就需要用到 interface 的变体。比如,支持比较的类型可以写成这样:
type Ordered interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 | uintptr |
float32 | float64 |
string
}
所以,前面的Max
函数应该改写成Max[T ordered](a, b T) T
。因为添加了 ordered 限制,所以Max([]int{1}, []int{2})
在编译的时候就会报错。
定义 constraint 的时候还支持一种特殊的语法
type Float interface {
~float32 | ~float64
}
这里的~
表示底层类型为float32
或者float64
。也就是说,如果你定义type MyFloat float32
,也是可以匹配Float
的。但如果不加~
,那就无法匹配。
这里用到的 interface,大家应该能猜到可以使用接口来限制泛型参数的范围(必须实现相应的接口)。那如果要支持传入所有类型(也就是不加限制),那就得使用大名鼎鼎的interface{}
了。但是写成func foo[T interface{}])(a T)
实在是难看,而且T
后面必须加限制,多数泛型的参数限制又都是interface{}
,所以官方才同意引入关键词any
来代替interface{}
。这就回应了最前面Max[T any](a, b T) T
的写法。
泛型详细介绍可以参考我的另一篇文章Go语言泛型的进化。
标准库的泛型改造
有了泛型,很多之前做不了或不优雅的事情现在都好做了。
引入新的标准库
为了方便大家写泛型代码,首先要引入的就是 constraints 标准包。
package constraints
// 有符号整数
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// 无符号整数
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// 所有整数
type Integer interface {
| Unsigned
Signed }
// 浮点数
type Float interface {
~float32 | ~float64
}
// 复数
type Complex interface {
~complex64 | ~complex128
}
// 支持比较的类型
type Ordered interface {
| Float | ~string
Integer }
// 任意类型的 slice
type Slice[Elem any] interface {
~[]Elem
}
// 任意类型的 map
type Map[Key comparable, Val any] interface {
~map[Key]Val
}
// 任意类型的 channel
type Chan[Elem any] interface {
~chan Elem
}
其中 comparable 是内置泛型限制,匹配所有允许相等比较的类型。但 constraints 包刚合并,fzipp 就提议简化泛型限制语法。简单来说就是让[T nonInterfaceType]
等价于 [T interface{~nonInterfaceType}]
。举个例子:
匹配所有 map 的泛型参数可以写成[M interface{~map[K]V}, K comparable, V any]
,也可引用 constraints 包写成[M constraints.Map[K, V], K comparable, V any]
。但如果支持fzipp的提案,就可以写成[M map[K]V, K comparable, V any]
。同样的,如果想匹配所有 slice,可以写成[S []T, T any]
。如果只是想简单匹配多个类型,可以写成[T int8|int16]
,是不是有点 union type 的意思。
我们可以不关心 fzipp 提案的细节,但有一点需要明确,如果接受了 fzipp 的提案,那 constraints 包中的 Slice/Map/Chan 就没太大的必要了。这是就 Rob 担心的地方。我们需要现实的经验来检验当前泛型的设计。
同样引入的还有 slice 操作包:
package slices
import "constraints"
// 相等
func Equal[T comparable](s1, s2 []T) bool {}
func EqualFunc[T1, T2 any](s1 []T1, s2 []T2, eq func(T1, T2) bool) bool {}
// 比较
func Compare[T constraints.Ordered](s1, s2 []T) int {}
func CompareFunc[T1, T2 any](s1 []T1, s2 []T2, cmp func(T1, T2) int) int {}
// 查找
func Index[T comparable](s []T, v T) int {}
func IndexFunc[T any](s []T, f func(T) bool) int {}
func Contains[T comparable](s []T, v T) bool {}
// 插入
func Insert[S ~[]T, T any](s S, i int, v ...T) S {}
// 删除
func Delete[S ~[]T, T any](s S, i, j int) S {}
// 复制
func Clone[S ~[]T, T any](s S) S {}
// 去重
func Compact[S ~[]T, T comparable](s S) S {}
func CompactFunc[S ~[]T, T any](s S, eq func(T, T) bool) S {}
// 扩容
func Grow[S ~[]T, T any](s S, n int) S {}
// 缩容
func Clip[S ~[]T, T any](s S) S {}
除了Index/Contains/Insert
比较常用外,其他方法都非常小众。像Grow
的代码只有下面的一行:
func Grow[S ~[]T, T any](s S, n int) S {
return append(s, make(S, n)...)[:len(s)]
}
它们具体在实践中的效果也不得而知,现在就集成到标准库确实为时尚早。
同样引入的map操作:
package maps
// 所有 key
func Keys[M constraints.Map[K, V], K comparable, V any](m M) []K
// 所有值
func Values[M constraints.Map[K, V], K comparable, V any](m M) []V
// 判断相等
func Equal[M1, M2 constraints.Map[K, V], K, V comparable](m1 M1, m2 M2) bool
func EqualFunc[M1 constraints.Map[K, V1], M2 constraints.Map[K, V2], K comparable, V1, V2 any](m1 m1, m2 M2, cmp func(V1, V2) bool) bool
// 清空
func Clear[M constraints.Map[K, V], K comparable, V any](m M)
// 复制
func Clone[M constraints.Map[K, V], K comparable, V any](m M) M
// 把一个 map 的所有 k-v 复制到另一个
func Copy[M constraints.Map[K, V], K comparable, V any](dst, src M)
// 删除指定元素
func DeleteFunc[M constraints.Map[K, V], K comparable, V any](m M, del func(K, V) bool)
这里面比较常用的就是Keys/Values
了,其他函数需要通过实践进一步验证。
以上都是引入新标准库的部分,不会对已有的代码产生兼容性问题。还有一部分需要改造已有的标准库,这可能会产生兼容性问题。
改造旧的标准库
这一部分主要可以参考 Russ Cox 发起的讨论how to update APIs for generics。问题的核心是很多包已经把好的名字占用了,没法直接改造成支持泛型的版本。比如:
- sync.Pool 的类型应该定义成 sync.Pool[T]
- sync.Map 的类型应该定义成 sync.Map[K, V]
- atomic.Value 的类型应该定义成 atomic.Value[T]
- list.List 的类型应该定义成 list.List[T]
- math.Abs 应该定义成 math.Abs[T]
- math.Min 应该定义成 math.Min[T]
- math.Max 应该定义成 math.Max[T]
很可惜,这些类型或者函数都被强关联了interface{}
。
以 sync.Pool 为例,如果强行改成sync.Pool[T]
,那所有声明 Pool 的地方都需要改成var p sync.Pool[interface{}]
才能正常工作。
而对于 math.Max 就种,如果直接改为泛型,则又会因为类型推导产生问题。比如:
var i int
(i, 5) // 对应 Min[int]
Min(3, 5) // 对应 Min[float64] Min
所以 Russ 提议给相关的类型或者函数加一个带 Of 后缀的版本!比如,sync.Pool
对应sync.PoolOf[T any]
。sync 包的提案在这里。这种容器类的加 Of 后后缀也就罢了,math.Min 这种也要写成 math.MinOf 感觉有点怪。
当然了,还有人提议给标准库添加 v2 后缀或者前缀,甚至还为了用 v2/math 还是 math/v2 吵起来。
我个人更倾向于 DeedleFake 的方案,不改名字,而是根据 go.mod 的 go 版本决定是否使用泛型代码。如果有人想升级到 go 1.18,只要运行一遍 go fix 就可以把所有代码升级到泛型版本。我认为这是最清真的方案。
总结
内容够多了,回到 Rob 的提案。经过前文的分析,我们很容易得出这样的结论:我们缺少泛型相关的实践经验,现在仓促修改标准库只会给社区带来更多的维护负担。所以 Rob 的提案虽然略显保守,却也非常科学。我支持 Rob。最后以引用 Rob 的评论结束本文:I strongly believe it is best to take it slow for now. Use, learn, study, and move cautiously.