时间过得飞快,不知不觉间春节假期已经过完了,我们又返回到工作岗位上。就在除夕当晚,Go1.18的第二个beta版发布了,正式版推迟到了3月份发布。之前我们曾经简单介绍过go1.17的泛型尝鲜,但是由于实现不完整,所以有太多的限制,也就没有太深入的去分析。这次的beta版到正式发布应该不会有重大变化了,所以我们赶在正式版发布之前再来聊聊泛型。
所谓的泛型,实际上是一种抽象,在Go语言里被称为类型参数type parameters
,这种叫法更易于理解。类型参数,通俗来讲就是变量的类型是参数化的,是可以通过参数来指定的。我们在定义一个函数或者数据类型的时候,可以不指定某些参数和变量的具体类型,而是预留数据类型占位符。等到实际用到这些函数或自定义数据类型时,再为这些占位符指定实际的数据类型。通过下面的例子很容易理解:
1 | func Print[T any](v T) { |
其中T
就是类型参数。与C++的模板以及Java的泛型相比,Go语言的泛型看起来有着诸多不同。首先Go没有像C++和Java那样,使用一对尖括号<>
来装载类型参数,而是选择了中括号[]
,这是为了避免和小于号之间造成歧义。这样设计可以避免编译器实现方面的不必要的复杂性,从而保证编译速度,这一点是Go团队非常看重的。其次就是类型参数T
后面的这个any
,这在C++、Java等语言的泛型实现中也是没有的,any
在这里被称为“约束条件”。
约束条件
使用过C++模板的同学应该或多或少有过一些痛苦经历,那就是解决模板相关的编译错误。尤其是在C++11标准之前,经过几层模板类和模板函数的嵌套之后,编译器输出的错误信息往往会让你不知所云。就是因为C++模板在设计之初没有提供一种简单易用的声明式约束,编译器必须遍历所有与之相关的语句之后,才能知道实际传入的类型参数是否满足要求。在经过多层嵌套之后,报错的位置竟然时常出现在标准库及其他一些第三方库里,着实让人头疼不已,甚至怀疑人生。
Go语言引入了用于类型参数的约束条件constraints
,很好的解决了这一问题。约束条件在语法层面通过接口interface
来实现,这是一个被扩展过的接口,我们在后文中会介绍具体是如何扩展的,这里先重点关注约束条件。约束条件会作用于两个方面,首先是泛型函数或自定义类型内部,只能使用约束条件所允许的操作。另一方面是函数的调用者、以及自定义类型被使用的地方,只能传入满足约束条件的类型参数。比如以下示例:
1 | func ToString[T fmt.Stringer](v T) string { |
这个函数没什么实际价值,fmt.Stringer
就是约束条件,所以ToString
函数内部可以调用v.String
方法,如果使用了fmt.Stringer
接口不包含的方法就会报错,无法通过编译。同理,在调用者方面,传入的类型参数必须实现了fmt.Stringer
接口,否则无法通过编译。得益于约束条件,编译器能够非常高效的检查类型参数的合法性,输出的错误提示也更加直观。
any
对约束条件的概念有了基本的理解之后,我们再回过头来看这个约束条件any
。它其实就是空接口interface{}
的别名,go1.18标准库中所有的interface{}
都被替换成了any
,有兴趣的同学可以看看fmt.Print
系列函数的参数类型。我们之前说过,interface{}
是万能容器类型,从接口的角度来理解,不包含任何方法也就是对于具体类型没有任何要求,所以支持任意类型。用作约束条件时,也就相当于没有任何约束,所以支持任意类型。
根据Type Parameters Proposal的相关描述,Go语言的泛型在设计上,对于任意类型支持以下操作:
- 声明该类型的变量
- 相同数据类型的值和变量间的赋值操作
- 作为函数的参数和返回值
- 取该类型变量的地址
- 把该类型的值转换为或赋值给
interface{}
- 把
T
类型的值转换为T
类型,允许但没什么用 - 用作类型断言的目标类型
- 用在
type switch
的某个case
分支中 - 定义复合类型时作为复合类型的组成部分,比如作为切片的元素类型
- 把该类型传递给某些预定义的函数,比如
new
以上这些操作是在语言层面对任意类型都支持的,任何约束条件都适用。对于特定的约束条件,除了以上这些操作外,还可以使用约束条件允许的操作,比如上述ToString
函数中fmt.Stringer
接口所支持的String
方法。关于泛型和约束条件的基本概念就讲这些,我们在下一篇文章中详细介绍约束条件的用法。