虽然泛型是开发人员表达“通用代码”的一种重要方式,但这并不意味着所有泛型代码对所有类型都适用。更多的时候,我们需要对泛型函数的类型参数以及泛型函数中的实现代码设置限制。泛型函数调用者只能传递满足限制条件的类型实参,泛型函数内部也只能以类型参数允许的方式使用这些类型实参值。在 Go 泛型语法中,我们使用类型参数约束(type parameter constraint)(以下简称约束)来表达这种限制条件。
约束之于类型参数就好比函数参数列表中的类型之于参数:
函数普通参数在函数实现代码中可以表现出来的性质与可以参与的运算由参数类型限制,而泛型函数的类型参数就由约束(constraint)来限制。
2018 年 8 月由伊恩·泰勒和罗伯特·格瑞史莫主写的 Go 泛型第一版设计方案中,Go 引入了 contract 关键字来定义泛型类型参数的约束。但经过约两年的 Go 社区公示和讨论,在 2020 年 6 月末发布的泛型新设计方案中,Go 团队又放弃了新引入的 contract 关键字,转而采用已有的 interface 类型来替代 contract 定义约束。这一改变得到了 Go 社区的大力支持。使用 interface 类型作为约束的定义方法能够最大程度地复用已有语法,并抑制语言引入泛型后的复杂度。
但原有的 interface 语法尚不能满足定义约束的要求。所以,在 Go 泛型版本中,interface 语法也得到了一些扩展,也正是这些扩展给那些刚刚入门 Go 泛型的 Go 开发者带来了一丝困惑,这也是约束被认为是 Go 泛型的一个难点的原因。
下面我们来看一下 Go 类型参数的约束, Go 原生内置的约束、如何定义自己的约束、新引入的类型集合概念等。我们先来看一下 Go 语言的内置约束,从 Go 泛型中最宽松的约束:any 开始。
二、最宽松的约束:any
if v == zero { // 编译器报错:invalid operation: v == zero (incomparable types in type set)
continue
}
ret = append(ret, v.String())
}
return ret
}
复制代码
我们看到,针对 v 的相等性判断导致了编译器报错,我们需要为类型参数赋予更多的能力,比如支持相等性和不等性比较。这让我们想起了我们刚刚学过的 Go 内置约束 comparable,实现 comparable 的类型,便可以支持相等性和不等性判断操作了。
我们知道,comparable 虽然不能像普通接口类型那样声明变量,但它却可以作为类型嵌入到其他接口类型中,下面我们就扩展一下上面示例:
func StringifyLessThan[T Stringer](s []T, max T) (ret []string) {
var zero T
for _, v := range s {
if v == zero || v >= max {
continue
}
ret = append(ret, v.String())
}
return ret
}
复制代码
但现在当我们编译上面 StringifyLessThan 函数时,我们会得到编译器的报错信息 invalid operation: v >= max (type parameter T is not comparable with >=)。Go 编译器认为 Stringer 约束的类型参数 T 不具备排序比较能力。
如果连排序比较性都无法支持,这将大大限制我们泛型函数的表达能力。但是 Go 又不支持运算符重载(operator overloading),不允许我们定义出下面这样的接口类型作为类型参数的约束:
[code]type Stringer[T any] interface { String() string comparable >(t T) bool >=(t T) bool