基本语法
定义可接受类型列表(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 int64func 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}
// Benchmarkfunc 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.70GHzBenchmarkMaxIntBenchmarkMaxInt-4 1000000000 0.4331 ns/opBenchmarkMaxGenBenchmarkMaxGen-4 1000000000 0.4276 ns/opBenchmarkMaxIGenBenchmarkMaxIGen-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.70GHzBenchmarkAddPolyBenchmarkAddPoly-4 146483520 8.360 ns/opBenchmarkAddGenBenchmarkAddGen-4 100000000 11.83 ns/op
复制代码
这种情况下范型的性能明显要差于原有的多态实现。所以,类似情况最好不要使用范型来替代多态,不仅没有简化程序,反而对性能有一定影响。
最后,写给 Java 程序员“关于类型擦除”
Java 程序员都知道,Java 的泛型实现是编译时期的,在运行时是被擦除的,例如:
// Example generic classpublic 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语言入门到实践》
评论