基本语法
定义可接受类型列表(constraint)
简单来说,Go 的泛型就是将参数类型定义为一个可接受类型的列表(称为 constraint),直接上例子。
例 1: 方法定义上使用范型
func Max[T int|int32|int64|float32|float64] (a, b T) T {
if a > b {
return a
}
return b
}
复制代码
例 2: 结构类型定义上使用范型
type Node[T any] struct{
Value T
Next *Node[T]
}
func (n *Node[T]) Append(v T) *Node[T]{
next := &Node[T]{v,nil}
n.Next = next
return next
}
复制代码
以上例子中使用了一个预置的 constraint “any”,这里 any 就相当于 interface{},表示任何类型。
另一个预置的 constraint 是 comparable,代表可以支持==和!=比较的类型。
让列表更简单
用 “~”简化衍生类型的声明
上面范型函数只能接受 constraint 列表中的类型作为调用的参数,包括这些类型的别名。但对于列表中类型的衍生类型就不能接受了。
例如:
type MyInt int64
func TestMax(t *testing.T) {
var a,b MyInt = 2,3
Max(a,b)
}
复制代码
编译以上程序会引发编译错误,在 Go 中为了简化扩展类型的定义,我们可以通过"~"来表示支持类型扩展类型。利用这种方式修改方法定义即可修复上面的编译问题。
func Max[T int|int32|~int64|float32|float64] (a, b T) T {
if a > b {
return a
}
return b
}
复制代码
定义 constraint 类型
虽然有了上面的对于扩展类型的简化定义方式,但还是会很容易产生很长的 contraint 类型列表,而且这个列表往往会需要在很多方法定义中重复。
为了简化和复用复杂的类型列表(constraint)声明,在 Go1.18 中可以通过 interface 来定义一个 constraint 类型。
1 interface 中定义 constraint 类型支持的方法
这种方式下方法所接受的类型必须要实现了该接口定义,即实现了该接口中包含的方法。
type Addable interface {
Add(a Addable) Addable
}
type type1 struct {
value int64
}
func (t type1) Add(a Addable) Addable {
b,_:=a.(type1)
return type1{t.value + b.value}
}
type type2 struct {
value float64
}
func (t type2) Add(a Addable) Addable {
b,_:=a.(type2)
return type2{t.value + b.value}
}
func AddGen[T Addable](a T, b T) Addable {
return a.Add(b)
}
复制代码
这里你可能会有一个问题,那么既然有使用了 interface,那完全可以使用多态--以接口 Addable 作为方法的参数,定义如下:
func AddPoly(a Addable, b Addable) Addable {
return a.Add(b)
}
复制代码
问题来了,这里的这个 AddPoly 和上面使用范型定义的 AddGen 一样吗?
答案是不一样的,
AddGen 的传入参数(a,b) 必须是同一类型,而不仅仅是都实现了 Addable 接口,上列中要么都是 type1,要么就都是 type2。
而 AddPoly 方法就不同了,传入参数只要是实现了 Addable 就行,即(a,b)可以一个是 type1,另一个是 type2。
2 interface 中直接包含可接受类型列表
这里就比较直接可以在一个 interface 中定义可接受类型的列表
type SupportTypes interface{
int | int32 | ~int64 | float32 | float64
}
func Max[T SupportTypes] (a, b T) T {
if a > b {
return a
}
return b
}
复制代码
这个 constraint interface 可以在不同的地方被复用,Google 在"golang.org/x/exp/constraints"包中也贴心的提供了很多的常用的 constraint interface。
关于性能
Go 的泛型实现影响性能吗?这是很多程序员最关心的问题。我们直接通过 Benchmark 程序来看结果。
1 constraint 为基础计算类型
type SupportTypes interface{
int | int32 | ~int64 | float32 | float64
}
func Max[T int | int32 | ~int64 | float32 | float64] (a, b T) T {
if a > b {
return a
}
return b
}
func MaxI[T SupportTypes] (a, b T) T {
if a > b {
return a
}
return b
}
func MaxInt(a, b int64) int64 {
if a > b {
return a
}
return b
}
// Benchmark
func BenchmarkMaxInt(b *testing.B) {
a := rand.Int63n(1000)
c := rand.Int63n(1000)
b.ResetTimer()
for i:=0;i<b.N;i++{
_ = MaxInt(a,c)
}
}
func BenchmarkMaxGen(b *testing.B) {
a := rand.Int63n(1000)
c := rand.Int63n(1000)
b.ResetTimer()
for i:=0;i<b.N;i++{
_ = Max(a,c)
}
}
func BenchmarkMaxIGen(b *testing.B) {
a := rand.Int63n(1000)
c := rand.Int63n(1000)
b.ResetTimer()
for i:=0;i<b.N;i++{
_ = MaxI(a,c)
}
}
复制代码
Benchmark 结果
cpu: Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz
BenchmarkMaxInt
BenchmarkMaxInt-4 1000000000 0.4331 ns/op
BenchmarkMaxGen
BenchmarkMaxGen-4 1000000000 0.4276 ns/op
BenchmarkMaxIGen
BenchmarkMaxIGen-4 1000000000 0.4353 ns/op
复制代码
可以看出 Go 泛型对于基本类型的影响几乎没有。
2 constraint 为接口定义的类型(这里指类型需要实现接口中的方法)
type Addable interface {
Add(a Addable) Addable
}
type type1 struct {
value int64
}
func (t type1) Add(a Addable) Addable {
b,_:=a.(type1)
return type1{t.value + b.value}
}
type type2 struct {
value float64
}
func (t type2) Add(a Addable) Addable {
b,_:=a.(type2)
return type2{t.value + b.value}
}
func AddPoly(a Addable, b Addable) Addable {
return a.Add(b)
}
func AddGen[T Addable](a T, b T) Addable {
return a.Add(b)
}
var a type1 = type1{1}
var c type1 = type1{1}
func BenchmarkAddPoly(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
AddPoly(a,c)
}
}
func BenchmarkAddGen(b *testing.B) {
b.ResetTimer()
for i:=0;i<b.N;i++{
AddGen(a,c)
}
}
复制代码
Benchmark 的结果
cpu: Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz
BenchmarkAddPoly
BenchmarkAddPoly-4 146483520 8.360 ns/op
BenchmarkAddGen
BenchmarkAddGen-4 100000000 11.83 ns/op
复制代码
这种情况下范型的性能明显要差于原有的多态实现。所以,类似情况最好不要使用范型来替代多态,不仅没有简化程序,反而对性能有一定影响。
最后,写给 Java 程序员“关于类型擦除”
Java 程序员都知道,Java 的泛型实现是编译时期的,在运行时是被擦除的,例如:
// Example generic class
public class ArrayList<E extends Number> {
// ...
};
ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if (li.getClass() == lf.getClass()) { // 这里是true
System.out.println("Equal");
}
复制代码
由于类型擦除的缘故,li.getClass() == lf.getClass() 的结果是 true。
而 Go 的泛型则不是通过这种方式实现的,Go 采用了一种不完全的单态化(monomorphization)称之为“GCShape stenciling with Dictionaries” 。这种实现方式也导致了我们在前面看到的性能测试的结果,这里我就不展开了,未来在别的文章中和大家讨论。
type Numeric interface {
int | int64 | uint | float32 | float64
}
type ArrayList[T Numeric] struct {}
var li ArrayList[int64]
var lf ArrayList[float64]
if reflect.TypeOf(li) == reflect.TypeOf(lf) { // 这里是false
fmt.Printf("they're equal")
return
}
复制代码
由于没有类型擦除,reflect.TypeOf(li) == reflect.TypeOf(lf)结果返回为 false。
更多关于 Go 语言的编程分享可以观看我的视频 《Go语言入门到实践》
评论