基本语法
定义可接受类型列表(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 TNext *Node[T]}func (n *Node[T]) Append(v T) *Node[T]{next := &Node[T]{v,nil}n.Next = nextreturn next}
以上例子中使用了一个预置的constraint “any”,这里any就相当于interface{},表示任何类型。
另一个预置的constraint是comparable,代表可以支持==和!=比较的类型。
让列表更简单
用 “~”简化衍生类型的声明
上面范型函数只能接受constraint列表中的类型作为调用的参数,包括这些类型的别名。但对于列表中类型的衍生类型就不能接受了。
例如:
type MyInt int64func TestMax(t *testing.T) {var a,b MyInt = 2,3Max(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)}}
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)}}
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()) { // 这里是trueSystem.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) { // 这里是falsefmt.Printf("they're equal")return}
由于没有类型擦除,reflect.TypeOf(li) == reflect.TypeOf(lf)结果返回为false。
文章评论