全面解读!Golang中泛型的使用

共 21125字,需浏览 43分钟

 ·

2022-06-27 02:42


导语 | Golang在2022-03-15发布了V1.18正式版,里面包含了对泛型的支持,那么最新版本的泛型如何使用呢?有哪些坑呢?本文全面且详细的带你了解泛型在Golang中的使用。


一、什么是泛型


说起泛型这个词,可能有些人比较陌生,特别是PHP或者JavaScript这类弱语言的开发者,尤其陌生。因为在这些弱语言中,语法本身就是支持不同类型的变量调用的。可以说无形之中早已把泛型融入语言的DNA中了,以至于开发者习以为常了。


举个PHP中的泛型的例子:


我们定义了一个sum函数,参数是传入2个变量,返回值是2个变量的求和。


function sum($a, $b) {  return $a+$b;}


你可以在PHP中这样调用这个函数:


sum(1, 2); // return 3sum(1.23, 2.34);  //return 3.57sum("2.33", "54.222");  //return 56.552


我们可以传入不同类型的变量,可以是int, string, float型,这样一来,不仅精简了代码量,也使得开发者写出更加通用的代码逻辑。


那么回到标题,啥是泛型呢?一句话总结就是:定义一类通用的模板变量,可以传入不同类型的变量,使得逻辑更加通用,代码更加精简


但是!在Java,Golang,C++等这类静态语言中,是需要严格定义传入变量的类型的,并不能随心所欲。


举个Golang的例子:


func Sum(a, b int) int {  return a + b}


在函数Sum中,不仅需要严格定义传入参数a和b的变量类型,而且返回值的类型也需要严格定义。所以,你只能传入int类型,进行这样调用:


Sum(1, 2) // 3


如果你传入的是其他类型的变量,就会报错:


fmt.Println(Sum(1.23, 2.54)); 
./main.go:33:18: cannot use 1.23 (untyped float constant) as int value in argument to Sum (truncated)./main.go:33:24: cannot use 2.54 (untyped float constant) as int value in argument to Sum (truncated)


所以,Golang开发者如果想开发一个类似实现2个float类型变量相加的功能,只能另写1个函数:


func SumFloat(a, b float) float {  return a + b}


或者是写一个通用的Sum函数使用interface反射来判断。


func Sum(a, b interface{}) interface{} {  switch a.(type) {  case int:    a1 := a.(int)    b1 := b.(int)    return a1 + b1  case float64:    a1 := a.(float64)    b1 := b.(float64)    return a1 + b1  default:    return nil  }}


这样一来,不仅重复了很多的代码,而且频繁的类型转换,不仅繁琐性能低,而且在安全性上也不高。所以,Golang开发者希望官方在新版中增加泛型的特性支持,从这门语言诞生至今,呼吁声从未减少过



二、泛型的利和弊


然而泛型其实是一把双刃剑,既可能给开发者带来了便利,但是同样会带来编译和效率的问题。我们都知道,Golang不仅在编译速度还是性能上,口碑一直是非常不错的,如果引入泛型的语法,虽然便利了开发者,但是在语言的系统级别上,泛型是需要系统去推倒和计算变量的类型的,这在无形中会增加编译的时间和降低运行效率。


如何既能让开发者少写代码,又能让编绎时间不会过多增加,也不能让运行效率降低 ,这是Golang官方苦苦在追求的平衡点。


然而,几千年前,孟子就说过:“鱼和熊掌不可兼得”。所以,这个事情本身就很难。


我们先看下其他静态语言是如何实现泛型的。


C++是在编译期间类型特化实现泛型,但是编译器的实现变得非常复杂,会生成的大量重复代码,导致最终的二进制文件膨胀和编译缓慢。


Java是用类型擦除实现的泛型,编译器会插入额外的类型转换指令,会降低程序的执行效率。


那么Golang中是如何实现泛型的呢?



三、Golang中的泛型


千呼万唤始出来,官方在进行多次的提案和投票后,终于在2022年3月15日终于推出了支持泛型的版本: Go1.18。我们可以从官网下载对应我们操作系统的1.18+版本,并且升级一下最新的goland编辑器,以便更好的学习和错误提示。当然你也可以在gotip上https://gotipplay.golang.org/直接运行带有泛型的代码。


我们看下,在Golang 1.18版本中是如何利用泛型来实现上面的Sum函数的:


func Sum[T int|float64](a,b T) T {  return a + b}


然后,我们调用一下:


fmt.Println(Sum[int](1, 2))  //3fmt.Println(Sum[float64](1.23, 2.54))  //3.77


先不去理解这个函数中各个组件的含义,仅仅是看代码量就非常简洁,一个函数就实现了多个类型的功能。


下面我们就来仔细的了解一下泛型的语法。因为泛型针对的是类型变量,在Golang中,类型是贯穿整个语法生态的,比如:变量、函数、方法、接口、通道等等。我们就先从单独的泛型变量类型说起。



四、泛型变量


(一)泛型切片变量


我们可以这样定义1个泛型变量,比如,我们定义一个泛型切片,切片里的值类型,即可以是int,也可以是float64,也可以是string:


type Slice1 [T int|float64|string] []T


我们来仔细剖析一些这个写法。定义泛型和定义其他go类型一样,也是使用type关键字,后面的Slice1就是泛型变量名,后面紧接一个中括号[]。


我们重点看下Slice1[]里面的内容,它是定义泛型的核心:


  • T表示我们提炼出来的通用类型参数(Type parameter),是我们就用来表示不同类型的模板,T只是取的一个通用的名字,你可以取名任意其他名字都行。


  • 后面的int|float64|string 叫类型约束(Type constraint),也就是约束了T的取值范围,只能从(int、float64、string)中取值。中间的|表示的是或的关系,等于语法"||",所以你可以根据你类型的使用场景定义更多的类型约束。


  • []里面的这一串T int|float64|string,叫类型参数列表(type parameter list),表示的是我们定义了几个泛型的参数。我们例子当中只有1个,下面的例子中,我们会创建多个。


  • 最后面的[]T这个我们就很熟悉了,就是申请一个切片类型,比如常见的:[]int, []string 等等,只不过我们这里的类型是T,也就是参数列表里面定义的变量值。


我们把这整个类型,就叫做Slice1[T],它是一个切片泛型变量。


所以,总结一下:我们把需要用到的类型参数,提前在[]里进行定义,然后在后面实际的变量类型中进行使用,必须要先定义,后使用。


所以,上面的写法,我们按照它的类型约束的范围,拆开后,就等同这样:


type SliceInt []inttype SliceFloat []float64type SliceInt []string


是不是节省了大量的代码量。



(二)泛型map变量


同理,我们可以试着定义其他类型的泛型变量,定义Map1[KEY, VALUE]泛型变量,它是一个map类型的,其中类型参数KEY的类型约束是int|string,类型参数VALUE的类型约束为string|float64。它的类型参数列表有2个,是:KEY int|string, VALUE string| float64。


type Map1 [KEY int|string, VALUE string| float64] map[KEY]VALUE


我们拆开来看,它等同于下面的集合:


type Map2 map[int]stringtype Map3 map[int]float64type Map4 map[string]stringtype Map5 map[string]float64



(三)泛型结构体变量


同理,我们再创建1个结构体的泛型变量。其中的泛型参数T,有3个类型约束。


type Struct1 [T string|int|float64] struct {  Title string  Content  T}


拆开来看,它等于下面的集合:


type Struct3 struct {  Title string  Content  string}
type Struct4 struct { Title string Content int}
type Struct5 struct { Title string Content float64}



(四)泛型变量实例化


OK,我们弄清楚了如何定义一个泛型变量后,那么如何去实例化这个变量呢?我们先看下申明了一个普通的变量是如何实例化使用呢?


//申明一个int类型的变量MyInttype MyInt int
//实例化并赋值var int1 MyInit = 3
//打印fmt.Println(int1)


那我们也尝试这样子用泛型变量去实例化一下


//申明一个泛型切片type Slice1 [T int|float64|string] []T
//实例化,并赋值var MySlice Slice1[T] = []int{1,2,3}


我们运行后,是会报错的,提示T没定义。


./main.go:47:21: undefined: T


因为,在泛型里面,你如果去要实例化一个泛型变量,你需要去显示的申明实际传入的变量(也就是实参)是什么类型,用它去替换T。所以你得这样:


//申明一个泛型切片type Slice1 [T int|float64|string] []T
//实例化成int型的切片,并赋值,T的类型和后面具体值的类型保持一致。var MySlice1 Slice1[int] = []int{1,2,3}
//或者简写MySlice2 := Slice1[int]{1, 2, 3}
//实例化成string型的切片,并赋值, T的类型和后面具体值的类型保持一致。var MySlice3 Slice1[string] = []string{"hello", "small", "yang"}
//或者简写MySlice4 := Slice1[string]{"hello", "small", "yang"}

//实例化成float64型的切片,并赋值, T的类型和后面具体值的类型保持一致。var MySlice5 Slice1[float64] = []float64{1.222, 3.444, 5.666}
//或者简写MySlice6 := Slice1[float64]{1.222, 3.444, 5.666}


OK,当我们知道了如何去实例化1个泛型切片变量后,我们再来快速看一下,上面其他几个泛型变量的实例化。


map类型的泛型变量实例化


//申明type Map1[KEY int | string, VALUE string | float64] map[KEY]VALUE
//实例化:KEY和VALUE要替换成具体的类型。map里面的也要保持一致var MyMap1 Map1[int, string] = map[int]string{ 1: "hello", 2: "small",}
//或者这简写MyMap2 := Map1[int, string]{ 1: "hello", 2: "small",}
fmt.Println(MyMap1,MyMap2) // map[1:hello 2:small]
//实例化:KEY和VALUE要替换成具体的类型。map里面的也要保持一致var MyMap3 Map1[string, string] = map[string]string{ "one": "hello", "two": "small",}
//或者这样简写MyMap4 := Map1[string, string]{ "one": "hello", "two": "small",}fmt.Println(MyMap3, MyMap4) // map[one:hello two:small]


结构体泛型变量实例化:


//定义1个结构体泛型变量type Struct1 [T string|int|float64] struct {  Title string  Content  T}
//先实例化成float64var MyStruct1 Struct1[float64]
//再赋值MyStruct1.Title = "hello"MyStruct1.Content = 3.149
//或者这样简写var MyStruct2 = Struct1[string]{ Title: "hello", Content: "small",}
fmt.Println(MyStruct1,MyStruct2) //hello 3.149} {hello small}


说到结构体变量,在go里面是可以使用匿名的,即把结构体的申明定义和初始化一起完成,举个例子


stu := struct{  Name string  Age int  Weight float64}{  "smallyang",  18,  50.5,}fmt.Println("Student =", stu) // Student = {smallyang 18 50.5}


那么,泛型结构体变量,是否也支持匿名呢?我们来试一下:


stu2 := struct[T int|float64] {  Name   string  Age    int  Weight T}[int]{  "smallyang",  18,  50,}fmt.Println("Student =", stu2)


如果你在编辑器里写出这端代码,编辑器会直接标红,提示语法错误,也就是go无法识别这个匿名写法,不支持匿名泛型结构体,如果你运行一下,也会报错:


./main.go:70:16: syntax error: unexpected [, expecting {./main.go:72:10: syntax error: unexpected int at end of statement./main.go:73:10: syntax error: unexpected T at end of statement./main.go:74:3: syntax error: unexpected [ after top level declaration



(五)泛型变量嵌套


就像常量申明的变量类型支持嵌套一样,泛型变量也是支持嵌套的。我们把上面几种情况结合一下,来一个复杂点的例子:


在泛型参数列表中,我们定义了2个泛型变量,1个是S,另一个是嵌套了S的map泛型变量P


type MyStruct[S int | string, P map[S]string] struct {  Name    string  Content S  Job     P}


或许你现在应该很轻松的就知道如何去实例化了,值得注意的是,T和S要保持实参的一致。


//实例化int的实参var MyStruct1 = MyStruct[int, map[int]string]{  Name:    "small",  Content: 1,  Job:     map[int]string{1: "ss"},}
fmt.Printf("%+v", MyStruct1) // {Name:small Content:1 Job:map[1:ss]}
//实例化string的实参var MyStruct2 = MyStruct[string, map[string]string]{ Name: "small", Content: "yang", Job: map[string]string{"aa": "ss"},}
fmt.Printf("%+v", MyStruct2) //{Name:small Content:yang Job:map[aa:ss]}


我们再来看一下,稍复杂的例子,2个泛型变量之间的嵌套使用,Struct1这个结构体切片,它的第二个泛型参数的类型是Slice1。


//切片泛型type Slice1[T int | string] []T
//结构体泛型,它的第二个泛型参数的类型是第一个切片泛型。type Struct1[P int | string, V Slice1[P]] struct { Name P Title V}


这种情况,如何实例化呢?好像有点复杂的样子,无法下手。但是,万变不离其宗,请始终记住:在泛型里面,你如果去要实例化一个泛型变量,你需要去用实际传入的变量类型去替换T


明白了这个道理,应该就好下手了:


//实例化切片mySlice1 := Slice1[int]{1, 2, 3}
//用int去替换P, 用Slice1去替换Slice1[p]myStruct1 := Struct1[int, Slice1[int]]{ Name: 123, Title: []int{1, 2, 3},}
//用int去替换P, 用Slice1去替换Slice1[p]myStruct2 := Struct1[string, Slice1[string]]{ Name: "hello", Title: []string{"hello", "small", "yang"},}
fmt.Println(mySlice1, myStruct1, myStruct2) //[1 2 3] {123 [1 2 3]} {hello [hello small yang]}


最后再来看另一种嵌套的方式,看起来更复杂。直接来看这个例子:


type Slice1[T int|float64|string] []Ttype Slice2[T int|string] Slice1[T]


当然这个例子本身是没有任何的意义,我们只是抱着学习的角度去这样尝试,那么如何实例化呢?通过上面的学习,应该就很简单了:


mySlice1 := Slice1[int]{1, 2, 3, 4}mySlice2 := Slice2[string]{"hello", "small"}fmt.Println(mySlice1, mySlice2) //[1 2 3 4] [hello small]


你会发现,Slice2其实就是继承和实现了Slice1,也就是说Slice2的类型参数约束的取值范围,必须是在Slice1的取值范围里。我们可以尝试改一下:


type Slice1[T int|float64|string] []Ttype Slice2[T bool|int|string] Slice1[T]
mySlice1 := Slice1[int]{1, 2, 3, 4}mySlice2 := Slice2[bool]{true, false}


运行一下,会报错。会提示申明Slice2的这一行代码中的泛型参数T,没有实现Slice1中定义的3个泛型参数列表。也就得出了上面的结论。


./main.go:73:44: T does not implement int|float64|string


所以,我们可以继续尝试一下更加变态的嵌套写法:


type Slice1[T bool | float64 | string | int] []Ttype Slice2[T bool | float64 | string] Slice1[T]type Slice3[T bool | int] Slice2[T]


通过上面的解释,或许你就可以一眼看出问题再哪儿了,Slice3的取值范围,并不是再Slcie2的范围中,因为多了一个int类型。或许你会说,Slice1这个母变量的取值范围里就有int啊,为啥会报错呢?因为它是单一递归继承的,只会检查它的上一级的取值范围是否覆盖。



五、泛型函数


(一)泛型函数的申明


当我们深入了解了go中各个泛型变量的申明定义和实例化,以及个各种复杂的嵌套之后,我们接下来来了解一下,go中的用的最多的函数是如何运用泛型的。这就回到了我们文章最开始的那个例子:


计算2个数之和


func Sum[T int|float64](a,b T) T {  return a + b}


他的写法,和泛型变量写法其实基本类似,我们解刨一下:


  • Sum是函数名,这个和普通的函数一样。


  • Sum后面紧接着一个[],这个就是申明泛型参数的地方,和泛型变量一样,我们例子中只申请了1个参数类型T。


  • T后面接着的int | float64就是这个参数T的类型约束,也就是取值范围,这个和泛型变量一致。


  • []后面的(a,b T)是函数的调用参数,表示有2个参数,他们的类型都是T。


  • ()后面T则表示函数的返回值的类型,和普通函数的返回值写法一样,不过这里表示返回值的类型是T。



(二)泛型函数的调用


OK,当我们剖析完成之后,我们可以这样去调用一下这个函数:


//传入int的实参,返回值类型也是intintSum := Sum[int](1, 2)
//传入float64的实参,返回值类型也是float64float64Sum := Sum[float64](1.23, 2.45)
fmt.Println(intSum, float64Sum) //3 3.68


你会发现,泛型函数的调用和泛型变量实例化一样,就是得显示的申明一下实际的这个T,到底是什么类型的。


但是,这种调用写法也太奇怪了,完全不像是go语言,反倒是像是一门新语言一样,所以,贴心的go官方,允许你这样写:


intSum := Sum(1, 2)
float64Sum := Sum(1.23, 2.45)
fmt.Println(intSum, float64Sum) //3 3.68


是不是鹅妹子婴!这样一来,就彻底打破了普通函数和泛型函数的调用写法的沟壑,更加自然融为一体。其实这里也是利用了类型推导。


我们可以回忆一下,go里面的类型推导的用法:


a := 3 //  编译器自动推导 a 是int型变量b := "hello" // 编译器自动推导 b 是string型变量


那么这里调用泛型函数也就说的通了:


intSum := Sum(1, 2) // 自动推导出T 是int
float64Sum := Sum(1.23, 2.45) //自动推导出T是 float64


接下来,我们把泛型函数和泛型变量结合起来,看下这个复杂一点的例子:


func Foreach[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64](list []T) {  for _, t := range list {    fmt.Println(t)  }}


我们定义了一个类泛型型参数T,你会发现他的类型约束巨长,把数字类型都包括进来了,函数的作用是遍历打印这个[]T切片,功能很简单。


我们可以仔细看这个类型约束列表,你会觉得它非常长,不仅在编辑器中显示不全不美观,而且再重构或者维护的时候,也会出现问题。强迫症患者肯定会受不了。


那么有没有啥好的解决方案呢?既然这样问了,那么肯定是有的。答案就是:自定义类型约束



(三)自定义类型约束


直接上自定义的写法,看起来一下子就清爽了许多,有没有?


type MyNumber interface {  int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64}
func Foreach[T MyNumber](list []T) { for _, t := range list { fmt.Println(t) }}


我们仔细看下这个自定义类型约束的写法,你会发现,它用的是interface的写法。这个写法在go1.18之前的语法中,是来申明一个接口类型的。


比如,我们熟知的这个Error的例子:


type error interface {  Error() string}


那么如何分区,哪些是接口,哪些是自定义类型约束呢?这个我们接下来在泛型接口篇里来继续深挖。


自定义约束类型的并集


我们继续看这个自定义的类型约束。上面我们写了一个自定义的约束变量MyNumber,你有没有觉得,它还是依然是太长了,那么我们可以继续拆分,因为接口类型是支持嵌套使用的。我们可以继续拆分:


type myInt interface {    int | int8 | int16 | int32 | int64}
type myUint interface { uint | uint8 | uint16 | uint32}
type myFloat interface { float32 | float64}
func Foreach[T myInt| myUint | myFloat](list []T) { for _, t := range list { fmt.Println(t) }}


这样就进一步解耦了,3个类型独立分开,然后再函数Foreach的类型列表中,再用|进行集合,有点像是几个集合取并集。或者,我们可以进一步的操作:


type myInt interface {    int | int8 | int16 | int32 | int64}
type myUint interface { uint | uint8 | uint16 | uint32}
type myFloat interface { float32 | float64}
type myNumber interface { myInt | myUint | myFloat}
func Foreach[T myNumber](list []T) { for _, t := range list { fmt.Println(t) }}


这样就可以单独控制了,虽然代码量大了一些,但是总体的可读性和美观度以及后续的迭代都强了不少。


既然是各个集合的合集,那也可以单独合上某一个具体的变量类型,比如这样:


type myNumber interface {  myInt | myUint | myFloat | string}


三个自定义的约束类型,最后合并上了一个具体的string类型,这种快捷的写法也是可以的,这样就可以少写一个自定义的string类型的约束类型了。


自定义约束类型的交集


上面的各个自定义的约束类似都是采用交集的形式合并的,那么,它同样也可以采用交集的方式,只不过写法有一点区别,需要换行。


type myInt interface {    int | int8 | int16 | int32 | int64}
type myInt2 interface { int | int64}
type myFloat interface { float32 | float64}
//每一个自定义约束类型单独一行type myNumber interface { myInt myInt2}


这样,myNumber的约束类型就是取的是myInt和myInt2的交接,即myNumber的约束范围是:int|int64。那如果是2个没有交集的约束呢?


//每一个自定义约束类型单独一行type myNumber2 interface {  myInt  myFloat}


上面这个,我们肉眼就可感知,它俩没有交集的,也就是空集,即:没有任何数据约束类型。


func Foreach[T myNumber2](list []T) {  for _, t := range list {    fmt.Println(t)  }}
//调用一下。Foreach[int]([]int{1, 2, 3})Foreach[int8]([]int8{1, 2})Foreach[string]([]string{"hello", "small"})


我们如果用编辑器,编辑器就会提示提示错误了,提示是个它是空的约束,传任何类型都不行。因为go里面的任何值类型都不是空集,都是有类型的


Cannot use int as the type myNumber2 Type does not implement constraint 'myNumber2' because constraint type set is empty


(四)any\comparable\Ordered约束类型


你或多或少从一些文章或者文档里,看到过any这个约束类型。听这个单词的意思,好像是代表任何,比如下面这个例子:


func add[T any] (a, b T) {  }


通过上面的一系列分析,我们已经知道any就是代表一个类型约束,但是我们并没有定义过它,说明它是系统提供的,是一个全局可用的的。我们可以通过编辑器的跳转功能,查看下这个any的源码是怎么定义的。


/usr/local/go/src/builtin/builtin.go 里可以看到:


// any is an alias for interface{} and is equivalent to interface{} in all ways.type any = interface{}


上面的因为注释翻译一下就是说,any是interface{}的别名,它始终和interface{}是相等的。我们是知道当我们申明一个变量,如果它的类型是interface{},表示它可以是任何的变量类型。所以如果你愿意,你也可以使用any来申明:


//相等type MySmall interface{}type MySmall any
//相等scans := make([]interface{}, 6)scans := make([]any, 6)


你甚至可以通过全文搜索替换的方式,将老的代码中的interface{} 替换成any。


所以,总结一下,当你申明1个约束类似为any的时候,它表示的就是任意类型


但是有时候,any并不是万能可用的,比如,计算2个数之和,如果使用any约束的话,编辑器就会直接报错了:


func Sum[T any] (a, b T) T {  return a+b}
//报错:invalid operation: operator + not defined on a (variable of type T constrained by any)


我们分析一下,为啥会报错呢?因为go里面有些类型是不能进行+操作的。比如2个bool值,就无法进行+操作。那可能你会说,我实际传值的时候,我规避掉这些不能+的字符类型,不就可以了嘛?那当然不行。因为我们既然申请1个泛型变量,就相当于创建了一个通用的模板,是必须得满足所有的变量类型的。


所以,鉴于这种情况,官方又给我们搞了2个约束类型关键词:comparable和constraints.Ordered。从字母意思可以看得出来,前者是约束了可比较(==、!==),后者约束了可排序 (<、<=、>=、>)。


所以这两者结合起来,我们就可以实现比较2个数字的大小和相等关系了。


值得注意的是:Go官方团队在Go1.18 Beta1版本的标准库里因为泛型设计而引入了ontraints包。但是由于大家都泛滥的使用了,所以在go1.18正式版本中又将这个包又移除了,放入到扩展/x/exp里面了,想用的话,可以自行下载:


go get golang.org/x/exp/constraints

go: downloading golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdfgo: added golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf


我们看下怎么去申明一个可排序的泛型函数例子。


//导入constraints包import (  "fmt"  "golang.org/x/exp/constraints")
//T的约束类型是:constraints.Orderedfunc Max[T constraints.Ordered](a, b T) T { if a > b { return a } else { return b }}


这样,就约束好了,传入的T的实参,必须是可排序,也就是满足这几个:(<、<=、>=、>)。才能去调用实例化这个函数。我们去源码看下Orderd是怎么定义的


type Ordered interface {  Integer | Float | ~string}


可以很清晰的看出,它采用了自定义约束类型嵌套的方式,嵌套了好几个自定义的约束类型。最后的这个~string是啥意思呢?我们接下来会讲。


这样,我们就可以实例化调用这个Max函数了:


fmt.Println(Max[int](1, 2))  // 2fmt.Println(Max[float64](1.33, 2.44))  //2.44fmt.Println(Max[string]("hello", "small"))  //small
//省去传入的泛型变量的类型,由系统自行推导:fmt.Println(Max("4", "5")) // 5


说完了Orderd,我们快速的来看下comparable约束类型,这个目前是内置的,可通过编辑器调整看这个约束是如何定义的,可以看出比较的类型还挺多。


// comparable is an interface that is implemented by all comparable types// (booleans, numbers, strings, pointers, channels, arrays of comparable types,// structs whose fields are all comparable types).// The comparable interface may only be used as a type parameter constraint,// not as the type of a variable.type comparable interface{ comparable }


值得注意的是,这个comparable,是比较==或者!==,不能比较大小,别和Orderd搞混淆了,可以这样使用:


//比较boolfmt.Println(Match(true, true)) // ture
//比较numberfmt.Println(Match(1, 2)) //falsefmt.Println(Match(1.45, 2.67)) //false
//比较stringfmt.Println(Match("hello", "hello")) //true
//比较指针var age int = 28var sex int = 1p1 := &agep2 := &sexfmt.Println(Match(p1, p2)) //false
//channel 的比较ch1 := make(chan int, 1)ch2 := make(chan int, 1)fmt.Println(Match(ch1, ch2)) // true
//比较数组,注意!不能是切片fmt.Println(Match([2]int{1, 2}, [2]int{3, 4})) //false
//结构体的比较type MyStruct struct { Name string Age int}s1 := MyStruct{"yang", 18}s2 := MyStruct{"small", 18}fmt.Println(Match(s1, s2)) //false



(五)约束类型


上面我们在讲Ordered约束类型的时候,发现它最后合并上了一个~string,这个语法看着有点奇怪。如果熟悉PHP语言的人,应该是见过的,在PHP.ini里面设置错误显示的时候,表示排除掉。


表示显示除了警告之外的所有警告,是排除,减去的意思。


error_reporting(E_ALL & ~E_WARNING);


那么,在go泛型中,就不是这个意思了,它表示一个类型的超集。举个例子:


type MyInt interface {  ~int | ~int64}


则表示,这个约束的范围,不仅仅是int和int64本身,也包含只要最底层的是这2种类型的,都包含。那么啥时候会碰到这种情况呢?其实就是嵌套或者自定义类型的时候。



//申明1个约束范围type IntAll interface { int | int64 | int32}
//定义1个泛型切片type MySliceInt[T IntAll] []T
//正确:var MyInt1 MySliceInt[int]
//自定义一个int型的类型type YourInt int
//错误:实例化会报错var MyInt2 MySliceInt[YourInt]


我们运行后,会发现,第二个会报错,因为MySliceInt允许的是int作为类型实参,而不是YourInt, 虽然YourInt类型底层类型是int,但它依旧不是int类型)。


这个时候~就排上用处了,我们可以这样写就可以了,表示底层的超集类型。


type IntAll interface {  ~int | ~int64 | ~int32}



六、泛型方法


接下来,我们来看下go中如何泛型方法,首先需要指出的是go里面的方法指的是接收器类型(receiver type),我们经常会用这种方式来实现其他语言中类的作用。比如下面这个例子:


type DemoInt int
func (t DemoInt) methodName(param string) string {
}


我们看这种类型,不管是前面的(t DemoInt) 还是方法名后面参数 (param string) 里面都会涉及到具体的类型变量,所以都可以改造成泛型。我们先来看下接收器(t DemoInt) 如何改照成泛型。


(一)接收器泛型


我们先定义1个泛型变量,然后在这个变量上加上1个方法,试着写一下:


//申请一个自定义的泛型约束类型type NumberAll interface {  ~int|~int64|~int32|~int16|~int8|~float64|~float32}
//申请一个泛型切片类型,泛型参数是T,约束的类型是 NumberAlltype SliceNumber[T NumberAll] []T

//给泛型切片加上1个接收器方法func (s SliceNumber[T]) SumIntsOrFloats() T { var sum T for _, v := range s { sum += v } return sum}


注意一下 (s SliceNumber[T]) 这个写法,T后面是不用带上它的约束类型NumberAll的。然后返回值也是T类型。OK,这样之后,我们就完成了一个泛型接收器方法。


那么如何去调用呢?其实和普通的接收器方法是一样的,只不过我们得先去实例化泛型切片。


//实例化成intvar ss1 SliceNumber[int] = []int{1, 2, 3, 4}
//简化//ss1 := SliceNumber[int]{1, 2, 34}
ss1.SumIntsOrFloats() // 10
//实例化成float64var ss2 SliceNumber[float64] = []float64{1.11, 2.22, 3.33}
//简化//ss2 := SliceNumber[float64]{1.11, 2.22, 3.33}
ss2.SumIntsOrFloats() //6.66


这种泛型方法的运用,在实际上的很多场景都是很好用的的,比如不同类型的堆栈的入栈和出栈,这也是一个很经典的例子:


//自定义一个类型约束type Number interface{  int | int32 | int64 | float64 | float32 }

//定义一个泛型结构体,表示堆栈type Stack[V Number] struct { size int value []V}
//加上Push方法func (s *Stack[V]) Push(v V) { s.value = append(s.value, v) s.size++}
//加上Pop方法func (s *Stack[V]) Pop() V { e := s.value[s.size-1] if s.size != 0 { s.value = s.value[:s.size-1] s.size-- } return e}


我们就可以传入不同的类型数据去实例化调用一下:


//实例化成一个int型的结构体堆栈s1 := &Stack[int]{}
//入栈s1.Push(1)s1.Push(2)s1.Push(3)fmt.Println(s1.size, s1.value) // 3 [1 2 3]
//出栈fmt.Println(s1.Pop()) //3fmt.Println(s1.Pop()) //2fmt.Println(s1.Pop()) //1
//实例化成一个float64型的结构体堆栈s2 := &Stack[float64]{}s2.Push(1.1)s2.Push(2.2)s2.Push(3.3)fmt.Println(s2.Pop()) //3.3fmt.Println(s2.Pop()) //2.2fmt.Println(s2.Pop()) //1.1



(二)方法的参数泛型


说完接收器泛型之后,我们来看下第二种泛型的方式,就是方法的参数泛型,就是接收器是一个普通的类型,在方法的参数里面来设置泛型变量。我们尝试着写一下:


type DemoSlice []int
func (d DemoSlice) FindOne[T int](a T) bool { }


你会发现,你方法里面的逻辑都还没开始写,编辑器就会标红报错了:Method cannot have type parameters。方法不能有类型参数,即:方法是不支持泛型的。至少目前的1.18版本是不支持的。看后续版本会不会支持的。


既然,函数是支持泛型的,接收器也是支持函数的,所以我们把他们结合起来,稍加改造:


type DemoSlice[T int | float64] []T
func (d DemoSlice[T]) FindOne(a T) bool { for _, t := range d { if t == a { return true } } return false}
s1 := DemoSlice[int]{1, 2, 3, 4}fmt.Println(s1.FindOne(1))
s2 := DemoSlice[float64]{1.2, 2.3, 3.4, 4.5}fmt.Println(s2.FindOne(1.2))


七、泛型接口


上面大篇幅,我们花了很多时间基本把泛型的内容都说了一遍,应该是对go泛型有了一个较为深刻的认识了,回到前面抛出的关于自定义约束用interface的问题。


type error interface {  Error() string}

type DemoNumber interface { int | float64}


上面2个都采用interface申明,1个是传统的接口类型,1个是约束类型,法有啥区别呢?一个叫:方法集,另一个叫:类型集。其实他们本质上是一样的。传统的接口类型是,只要我实现了接口里面定义的方法,那我就是实现了这个接口。而约束类型,其实也是一样,只要我传入的值的类型,在你这约束范围内,那就是符合要求的。


所以,go在1.18版本后,对interface的定义改了,改成了:


接口类型定义了一个类型集合。接口类型的变量可以存储这个接口类型集合的任意一种类型的实例值。这种类型被称之为实现了这个接口。接口类型的变量如果未初始化则它的值为nil。


那如果把这2者结合起来呢?


type MyError interface {  int | float64  Error() string}


这种写法看着好陌生,里面既有约束类型,又有方法,这是go1.18中新增的写法,这种接口叫做:一般接口(General interface)。原先1.18之前的接口定义类型叫做:基本接口(Basic interfaces)。


所以总结一下:


  • 如果,1个接口里面只有方法,也就是老的语法写法,这个接口叫:基本接口。


  • 如果,1个接口里面,有约束类型的,有或者没有方法的,这个接口叫:一般接口。


不得不吐槽一下,这2个类型的名字取的真是有水准啊,跟没有区别一样,本身go里面的接口可以说是非常复杂的,这样一个改动后,简直是雪上加霜啊,直接把接口的难度等级又提升了一个等级。


(一)基本泛型接口


我们继续看下如何定义一个泛型接口呢?它的写法和泛型变量是类似的:


type MyInterface[T int | string] interface {  WriteOne(data T) T  ReadOne() T}


接口名字后面接一个[],里面填充的接口里面方法中需要用到的泛型参数。这个和定义其他泛型变量是一致的。然后接口里面就是具体的空方法了,和泛型函数或者泛型方法的写法一样。


但是值得注意的是,别写反了,别把泛型参数写到了方法的层面,这样是错误的语法:


//会提示错误:interface method must have no type parameterstype MyInterface interface {  WriteOne[T int | string] (data T) T  ReadOne[T int | string] () T}


当我们定义好了上面这个泛型接口,因为里面只有方法,没有约束类型的定义,所以它是个基本接口。那我们看下如何去实现这个基本泛型接口。


我们先定义1个普通的结构体类型,然后通过接收器方式绑定上2个方法:


type Note struct {
}
func (n Note) WriteOne(one string) string { return "hello"}
func (n Note) ReadOne() string { return "small"}


然后,我们看下如何实例化泛型接口,并且实现接口。这种写法和普通的实现接口的方式是一直的,只不过要显示的的传入T的值是什么。


var one MyInterface[string] = Note{}fmt.Println(one.WriteOne("hello"))fmt.Println(one.ReadOne())


值得注意的是泛型参数的值的类型,要和被实现的方法的参数值要保证一致,不然会报错:


//接口实例化用的是int,但是实现的方法里面都是string类型,并不匹配,无法被实现。var one MyInterface[int] = Note{}fmt.Println(one.WriteOne("hello"))fmt.Println(one.ReadOne())


报错如下:


 cannot use Note{} (value of type Note) as type MyInterface[int] in variable declaration:        Note does not implement MyInterface[int] (wrong type for ReadOne method)                have ReadOne() string                want ReadOne() int



(二)一般泛型接口


我们现在再来定义一个一般泛型接口,也就是说接口里面,有约束类型。看下怎么写:


type MyInterface2[T int | string] interface {  int|string
WriteOne(data T) T ReadOne() T}


那这种一般泛型接口如何实例化呢?我们试一试看看:


type Note2 int
func (n Note2) WriteOne(one string) string { return "hello"}
func (n Note2) ReadOne() string { return "small"}

var one MyInterface2[string] = Note{}


编辑器直接标红报错了。提示:


接口包含约束元素int和string,只能用作类型参数。


简而言之,一般泛型接口,只能被当做类型参数来使用,无法被实例化


type myInterface [T MyInterface2[int]] []T


但是这种这么变态的写法,如何实例化呢?这个有待研究,反正至少没报错了。


参考资料:

1.https://go.dev/ref/spec#Go_statements

2.https://go.dev/doc/tutorial/generics

3.https://blog.csdn.net/raoxiaoya/article/details/124322746



 作者简介


杨义

腾讯高级工程师

腾讯高级工程师,主要负责IEG游戏活动运营及高可用平台的建设,对云服务、k8s以及高性能服务上也有很深的了解。



 推荐阅读


C++尝鲜:在C++中实现LINQ!

C++异步从理论到实践!

C++反射:反射信息的自动生成!

小白入门级!webpack基础、分包大揭秘



浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报