Go 1.17中你就可以使用泛型了,可以参考我3月份的文章:Go 泛型尝鲜, 编译的时候需要加-gcflags=-G=3
参数,而当前master分支,默认已经支持泛型,不需要加-G=3
参数了。
你可以通过下面的步骤尝试go最新分支:
go get golang.org/dl/gotip
gotip download
编译代码的时候使用gotip
替换go
命令即可。
随着Go 1.17的发布,最近也涌现了很多的介绍Go泛型的文章,基本上都是简单介绍的文章。
最近Go泛型的变化是增加了两个操作符: ~
和|
:
an approximation element
~T
restricts to all types whose underlying type is T: 代表底层类型是T
a union element
T1 | T2 | ...
restricts to any of the listed elements: 代表或
,类型列表之一。这些不是我想介绍的内容,今天我肝一篇介绍Go泛型实现原理的文章,介绍Go泛型实现的方案。
对于一个函数func Echo[T any](t T){}
,Go编译器到底编译成了什么代码?
简单的说,当前Go泛型实现的方案和下图中的方案一样:
在国内的老破小小区的楼道中常见的一种高科技印刷技术,通过一个镂花模板,为每一种类型生成特化的类型,这个术语叫做stenciling
。
但是如果再说多一点,那么就应该从 Taylor和Griesemer说起。
Go泛型提案中关于泛型实现的介绍
Go的泛型有别于其它语言的方案,在Go语言中泛型叫做Type Parameter
(类型参数).
Taylor和Griesemer的提案Type Parameters Proposal更多的是泛型呈现形式和影响的思考,对具体的实现涉及甚少。
无论什么编程语言,根据Russ Cox的观察,实现泛型至少要面对下面三条困境之一,那还是在2009年:
拖累程序员:比如C语言,增加了程序员的负担,需要曲折的实现,但是不对增加语言的复杂性
拖累编译器: 比如C++编程语言,增加了编译器的负担,可能会产生很多冗余的代码,重复的代码还需要编译器斟酌删除,编译的文件可能非常大。Rust的泛型也属于这一类。
拖累执行时间:比如Java,将一些装箱成Object,进行类型擦除。虽然代码没啥冗余了,空间节省了,但是需要装箱拆箱操作,代码效率低。很显然, Go语言至简的设计哲学让它的泛型实现不会选择增加程序员的负担的道路,所以它会在第二和第三种方案中做选择。虽然提案中没有最终说明它选择了哪种方案,但是从实际编译的代码可以看出,它选择的是第二种方案。
三个方案
Keith H. Randall, MIT的博士,现在在Google/Go team做泛型方面的开发,提出了Go泛型实现的三个方案:
1.字典
在编译时生成一组实例化的字典,在实例话一个泛型函数的时候会使用字典进行蜡印(stencile)。
当为泛型函数生成代码的时候,会生成唯一的一块代码,并且会在参数列表中增加一个字典做参数,就像方法会把receiver当成一个参数传入。字典包含为类型参数实例化的类型信息。
字典在编译时生成,存放在只读的data p中。
当然字段可以当成第一个参数,或者最后一个参数,或者放入一个独占的寄存器。
当然这种方案还有依稀问题,比如字典递归的问题,更重要的是,它对性能可能有比较大的影响,比如一个实例化类型int
, x=y
可能通过寄存器复制就可以了,但是泛型必须通过memmove
。
2.蜡印(Stenciling)
或者翻译成用模板印
等。
就像下面的动图一样,同一个泛型函数,为每一个实例化的类型参数生成一套独立的代码,感觉和rust的泛型特化一样。
这种方案和上面的字典方案正好相反。
比如下面一个泛型方法:
func f[T1, T2 any](x int, y T1) T2 {
...
}
如果有两个不同的类型实例化的调用:
var a float64 = f[int, float64](7, 8.0)
var b struct{f int} = f[complex128, struct{f int}](3, 1+1i)
那么这个方案会生成两套代码:
func f1(x int, y int) float64 {
... identical bodies ...
}
func f2(x int, y complex128) struct{f int} {
... identical bodies ...
}
因为编译f时是不知道它的实例化类型的,只有在调用它时才知道它的实例化的类型,所以需要在调用时编译f。对于相同实例化类型的多个调用,同一个package下编译器可以识别出来是一样的,只生成一个代码就可以了,但是不同的package就不简单了,这些函数表标记为DUPOK
,所以链接器会丢掉重复的函数实现。
这种策略需要更多的编译时间,因为需要编译泛型函数多次。因为对于同一个泛型函数,每种类型需要单独的一份编译的代码,如果类型非常多,编译的文件可能非常大,而且性能也比较差。
3.混合方案(GC Shape Stenciling)
混合前面的两种方案。
对于实例类型的shape相同的情况,只生成一份代码,对于shape类型相同的类型,