全面解讀!Golang中泛型的使用
導語?|?
Golang在2022-03-15發(fā)布了V1.18正式版,里面包含了對泛型的支持,那么最新版本的泛型如何使用呢?
有哪些坑呢?
本文全面且詳細的帶你了解泛型在Golang中的使用。
一、什么是泛型
說起泛型這個詞,可能有些人比較陌生,特別是PHP或者JavaScript這類弱語言的開發(fā)者,尤其陌生。因為在這些弱語言中,語法本身就是支持不同類型的變量調用的??梢哉f無形之中早已把泛型融入語言的DNA中了,以至于開發(fā)者習以為常了。
舉個PHP中的泛型的例子:
我們定義了一個sum函數(shù),參數(shù)是傳入2個變量,返回值是2個變量的求和。
function sum($a, $b) {
return $a+$b;
}
你可以在PHP中這樣調用這個函數(shù):
sum(1, 2); // return 3
sum(1.23, 2.34); //return 3.57
sum("2.33", "54.222"); //return 56.552
我們可以傳入不同類型的變量,可以是int, string, float型,這樣一來,不僅精簡了代碼量,也使得開發(fā)者寫出更加通用的代碼邏輯。
那么回到標題,啥是泛型呢?一句話總結就是: 定義一類通用的模板變量,可以傳入不同類型的變量,使得邏輯更加通用,代碼更加精簡 。
但是!在Java,Golang,C++等這類靜態(tài)語言中,是需要嚴格定義傳入變量的類型的,并不能隨心所欲。
舉個Golang的例子:
func Sum(a, b int) int {
return a + b
}
在函數(shù)Sum中,不僅需要嚴格定義傳入?yún)?shù)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開發(fā)者如果想開發(fā)一個類似實現(xiàn)2個float類型變量相加的功能,只能另寫1個函數(shù):
func SumFloat(a, b float) float {
return a + b
}
或者是寫一個通用的Sum函數(shù)使用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開發(fā)者希望官方在新版中增加泛型的特性支持,從這門語言誕生至今,呼吁聲從未減少過 。
二、泛型的利和弊
然而泛型其實是一把雙刃劍,既可能給開發(fā)者帶來了便利,但是同樣會帶來編譯和效率的問題。我們都知道,Golang不僅在編譯速度還是性能上,口碑一直是非常不錯的,如果引入泛型的語法,雖然便利了開發(fā)者,但是在語言的系統(tǒng)級別上,泛型是需要系統(tǒng)去推倒和計算變量的類型的,這在無形中會增加編譯的時間和降低運行效率。
如何既能讓開發(fā)者少寫代碼,又能讓編繹時間不會過多增加,也不能讓運行效率降低? ,這是Golang官方苦苦在追求的平衡點。
然而,幾千年前,孟子就說過:“魚和熊掌不可兼得”。所以,這個事情本身就很難。
我們先看下其他靜態(tài)語言是如何實現(xiàn)泛型的。
C++是在編譯期間類型特化實現(xiàn)泛型,但是編譯器的實現(xiàn)變得非常復雜,會生成的大量重復代碼,導致最終的二進制文件膨脹和編譯緩慢。
Java是用類型擦除實現(xiàn)的泛型,編譯器會插入額外的類型轉換指令,會降低程序的執(zhí)行效率。
那么Golang中是如何實現(xiàn)泛型的呢?
三、Golang中的泛型
千呼萬喚始出來,官方在進行多次的提案和投票后,終于在2022年3月15日終于推出了支持泛型的版本: Go1.18。我們可以從官網(wǎng)下載對應我們操作系統(tǒng)的1.18+版本,并且升級一下最新的goland編輯器,以便更好的學習和錯誤提示。當然你也可以在gotip上 https://gotipplay.golang.org/ 直接運行帶有泛型的代碼。
我們看下,在Golang 1.18版本中是如何利用泛型來實現(xiàn)上面的Sum函數(shù)的:
func Sum[T int|float64](a,b T) T {
return a + b
}
然后,我們調用一下:
fmt.Println(Sum[int](1, 2)) //3
fmt.Println(Sum[float64](1.23, 2.54)) //3.77
先不去理解這個函數(shù)中各個組件的含義,僅僅是看代碼量就非常簡潔,一個函數(shù)就實現(xiàn)了多個類型的功能。
下面我們就來仔細的了解一下泛型的語法。因為泛型針對的是類型變量,在Golang中,類型是貫穿整個語法生態(tài)的,比如:變量、函數(shù)、方法、接口、通道等等。我們就先從單獨的泛型變量類型說起。
四、泛型變量
(一)泛型切片變量
我們可以這樣定義1個泛型變量,比如,我們定義一個泛型切片,切片里的值類型,即可以是int,也可以是float64,也可以是string:
type Slice1 [T int|float64|string] []T
我們來仔細剖析一些這個寫法。定義泛型和定義其他go類型一樣,也是使用type關鍵字,后面的Slice1就是泛型變量名,后面緊接一個中括號[]。
我們重點看下Slice1[]里面的內容,它是定義泛型的核心:
-
T表示我們提煉出來的通用類型參數(shù)(Type parameter),是我們就用來表示不同類型的模板,T只是取的一個通用的名字,你可以取名任意其他名字都行。
-
后面的int|float64|string 叫類型約束(Type constraint),也就是約束了T的取值范圍,只能從(int、float64、string)中取值。中間的|表示的是或的關系,等于語法"||",所以你可以根據(jù)你類型的使用場景定義更多的類型約束。
-
[]里面的這一串T int|float64|string,叫類型參數(shù)列表(type parameter list),表示的是我們定義了幾個泛型的參數(shù)。我們例子當中只有1個,下面的例子中,我們會創(chuàng)建多個。
-
最后面的[]T這個我們就很熟悉了,就是申請一個切片類型,比如常見的:[]int, []string 等等,只不過我們這里的類型是T,也就是參數(shù)列表里面定義的變量值。
我們把這整個類型,就叫做Slice1[T],它是一個切片泛型變量。
所以,總結一下: 我們把需要用到的類型參數(shù),提前在[]里進行定義,然后在后面實際的變量類型中進行使用,必須要先定義,后使用。
所以,上面的寫法,我們按照它的類型約束的范圍,拆開后,就等同這樣:
type SliceInt []int
type SliceFloat []float64
type SliceInt []string
是不是節(jié)省了大量的代碼量。
(二)泛型map變量
同理,我們可以試著定義其他類型的泛型變量,定義Map1[KEY, VALUE]泛型變量,它是一個map類型的,其中類型參數(shù)KEY的類型約束是int|string,類型參數(shù)VALUE的類型約束為string|float64。它的類型參數(shù)列表有2個,是:KEY int|string, VALUE string| float64。
type Map1 [KEY int|string, VALUE string| float64] map[KEY]VALUE
我們拆開來看,它等同于下面的集合:
type Map2 map[int]string
type Map3 map[int]float64
type Map4 map[string]string
type Map5 map[string]float64
(三)泛型結構體變量
同理,我們再創(chuàng)建1個結構體的泛型變量。其中的泛型參數(shù)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類型的變量MyInt
type 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
}
//先實例化成float64
var 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
(五)泛型變量嵌套
就像常量申明的變量類型支持嵌套一樣,泛型變量也是支持嵌套的。我們把上面幾種情況結合一下,來一個復雜點的例子:
在泛型參數(shù)列表中,我們定義了2個泛型變量,1個是S,另一個是嵌套了S的map泛型變量P
type MyStruct[S int | string, P map[S]string] struct {
Name string
Content S
Job P
}
或許你現(xiàn)在應該很輕松的就知道如何去實例化了,值得注意的是,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這個結構體切片,它的第二個泛型參數(shù)的類型是Slice1。
//切片泛型
type Slice1[T int | string] []T
//結構體泛型,它的第二個泛型參數(shù)的類型是第一個切片泛型。
type Struct1[P int | string, V Slice1[P]] struct {
Name P
Title V
}
這種情況,如何實例化呢?好像有點復雜的樣子,無法下手。但是,萬變不離其宗,請始終記?。?/span> 在泛型里面,你如果去要實例化一個泛型變量,你需要去用實際傳入的變量類型去替換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] []T
type 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]
你會發(fā)現(xiàn),Slice2其實就是繼承和實現(xiàn)了Slice1,也就是說Slice2的類型參數(shù)約束的取值范圍,必須是在Slice1的取值范圍里。我們可以嘗試改一下:
type Slice1[T int|float64|string] []T
type Slice2[T bool|int|string] Slice1[T]
mySlice1 := Slice1[int]{1, 2, 3, 4}
mySlice2 := Slice2[bool]{true, false}
運行一下,會報錯。會提示申明Slice2的這一行代碼中的泛型參數(shù)T,沒有實現(xiàn)Slice1中定義的3個泛型參數(shù)列表。也就得出了上面的結論。
./main.go:73:44: T does not implement int|float64|string
所以,我們可以繼續(xù)嘗試一下更加變態(tài)的嵌套寫法:
type Slice1[T bool | float64 | string | int] []T
type Slice2[T bool | float64 | string] Slice1[T]
type Slice3[T bool | int] Slice2[T]
通過上面的解釋,或許你就可以一眼看出問題再哪兒了,Slice3的取值范圍,并不是再Slcie2的范圍中,因為多了一個int類型。或許你會說,Slice1這個母變量的取值范圍里就有int啊,為啥會報錯呢?因為它是單一遞歸繼承的,只會檢查它的上一級的取值范圍是否覆蓋。
五、泛型函數(shù)
(一)泛型函數(shù)的申明
當我們深入了解了go中各個泛型變量的申明定義和實例化,以及個各種復雜的嵌套之后,我們接下來來了解一下,go中的用的最多的函數(shù)是如何運用泛型的。這就回到了我們文章最開始的那個例子:
計算2個數(shù)之和
func Sum[T int|float64](a,b T) T {
return a + b
}
他的寫法,和泛型變量寫法其實基本類似,我們解刨一下:
-
Sum是函數(shù)名,這個和普通的函數(shù)一樣。
-
Sum后面緊接著一個[],這個就是申明泛型參數(shù)的地方,和泛型變量一樣,我們例子中只申請了1個參數(shù)類型T。
-
T后面接著的int | float64就是這個參數(shù)T的類型約束,也就是取值范圍,這個和泛型變量一致。
-
[]后面的(a,b T)是函數(shù)的調用參數(shù),表示有2個參數(shù),他們的類型都是T。
-
()后面T則表示函數(shù)的返回值的類型,和普通函數(shù)的返回值寫法一樣,不過這里表示返回值的類型是T。
(二)泛型函數(shù)的調用
OK,當我們剖析完成之后,我們可以這樣去調用一下這個函數(shù):
//傳入int的實參,返回值類型也是int
intSum := Sum[int](1, 2)
//傳入float64的實參,返回值類型也是float64
float64Sum := Sum[float64](1.23, 2.45)
fmt.Println(intSum, float64Sum) //3 3.68
你會發(fā)現(xiàn),泛型函數(shù)的調用和泛型變量實例化一樣,就是得顯示的申明一下實際的這個T,到底是什么類型的。
但是,這種調用寫法也太奇怪了,完全不像是go語言,反倒是像是一門新語言一樣,所以,貼心的go官方,允許你這樣寫:
intSum := Sum(1, 2)
float64Sum := Sum(1.23, 2.45)
fmt.Println(intSum, float64Sum) //3 3.68
是不是鵝妹子嬰!這樣一來,就徹底打破了普通函數(shù)和泛型函數(shù)的調用寫法的溝壑,更加自然融為一體。其實這里也是利用了類型推導。
我們可以回憶一下,go里面的類型推導的用法:
a := 3 // 編譯器自動推導 a 是int型變量
b := "hello" // 編譯器自動推導 b 是string型變量
那么這里調用泛型函數(shù)也就說的通了:
intSum := Sum(1, 2) // 自動推導出T 是int
float64Sum := Sum(1.23, 2.45) //自動推導出T是 float64
接下來,我們把泛型函數(shù)和泛型變量結合起來,看下這個復雜一點的例子:
func Foreach[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64](list []T) {
for _, t := range list {
fmt.Println(t)
}
}
我們定義了一個類泛型型參數(shù)T,你會發(fā)現(xiàn)他的類型約束巨長,把數(shù)字類型都包括進來了,函數(shù)的作用是遍歷打印這個[]T切片,功能很簡單。
我們可以仔細看這個類型約束列表,你會覺得它非常長,不僅在編輯器中顯示不全不美觀,而且再重構或者維護的時候,也會出現(xiàn)問題。強迫癥患者肯定會受不了。
那么有沒有啥好的解決方案呢?既然這樣問了,那么肯定是有的。答案就是:自定義類型約束
(三)自定義類型約束
直接上自定義的寫法,看起來一下子就清爽了許多,有沒有?
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)
}
}
我們仔細看下這個自定義類型約束的寫法,你會發(fā)現(xiàn),它用的是interface的寫法。這個寫法在go1.18之前的語法中,是來申明一個接口類型的。
比如,我們熟知的這個Error的例子:
type error interface {
Error() string
}
那么如何分區(qū),哪些是接口,哪些是自定義類型約束呢?這個我們接下來在泛型接口篇里來繼續(xù)深挖。
自定義約束類型的并集
我們繼續(xù)看這個自定義的類型約束。上面我們寫了一個自定義的約束變量MyNumber,你有沒有覺得,它還是依然是太長了,那么我們可以繼續(xù)拆分,因為接口類型是支持嵌套使用的。我們可以繼續(xù)拆分:
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個類型獨立分開,然后再函數(shù)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)
}
}
這樣就可以單獨控制了,雖然代碼量大了一些,但是總體的可讀性和美觀度以及后續(xù)的迭代都強了不少。
既然是各個集合的合集,那也可以單獨合上某一個具體的變量類型,比如這樣:
type myNumber interface {
myInt | myUint | myFloat | string
}
三個自定義的約束類型,最后合并上了一個具體的string類型,這種快捷的寫法也是可以的,這樣就可以少寫一個自定義的string類型的約束類型了。
自定義約束類型的交集
上面的各個自定義的約束類似都是采用交集的形式合并的,那么,它同樣也可以采用交集的方式,只不過寫法有一點區(qū)別,需要換行。
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
}
上面這個,我們肉眼就可感知,它倆沒有交集的,也就是空集,即:沒有任何數(shù)據(jù)約束類型。
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) {
}
通過上面的一系列分析,我們已經(jīng)知道any就是代表一個類型約束,但是我們并沒有定義過它,說明它是系統(tǒng)提供的,是一個全局可用的的。我們可以通過編輯器的跳轉功能,查看下這個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個數(shù)之和,如果使用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值,就無法進行+操作。那可能你會說,我實際傳值的時候,我規(guī)避掉這些不能+的字符類型,不就可以了嘛?那當然不行。因為我們既然申請1個泛型變量,就相當于創(chuàng)建了一個通用的模板,是必須得滿足所有的變量類型的。
所以,鑒于這種情況,官方又給我們搞了2個約束類型關鍵詞:comparable和constraints.Ordered。從字母意思可以看得出來,前者是約束了可比較(==、!==),后者約束了可排序?(<、<=、>=、>)。
所以這兩者結合起來,我們就可以實現(xiàn)比較2個數(shù)字的大小和相等關系了。
值得注意的是: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-0b5c67f07fdf
go: added golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf
我們看下怎么去申明一個可排序的泛型函數(shù)例子。
//導入constraints包
import (
"fmt"
"golang.org/x/exp/constraints"
)
//T的約束類型是:constraints.Ordered
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
} else {
return b
}
}
這樣,就約束好了,傳入的T的實參,必須是可排序,也就是滿足這幾個:(<、<=、>=、>)。才能去調用實例化這個函數(shù)。我們去源碼看下Orderd是怎么定義的 :
type Ordered interface {
Integer | Float | ~string
}
可以很清晰的看出,它采用了自定義約束類型嵌套的方式,嵌套了好幾個自定義的約束類型。最后的這個~string是啥意思呢?我們接下來會講。
這樣,我們就可以實例化調用這個Max函數(shù)了:
fmt.Println(Max[int](1, 2)) // 2
fmt.Println(Max[float64](1.33, 2.44)) //2.44
fmt.Println(Max[string]("hello", "small")) //small
//省去傳入的泛型變量的類型,由系統(tǒng)自行推導:
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搞混淆了 ,可以這樣使用:
//比較bool
fmt.Println(Match(true, true)) // ture
//比較number
fmt.Println(Match(1, 2)) //false
fmt.Println(Match(1.45, 2.67)) //false
//比較string
fmt.Println(Match("hello", "hello")) //true
//比較指針
var age int = 28
var sex int = 1
p1 := &age
p2 := &sex
fmt.Println(Match(p1, p2)) //false
//channel 的比較
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
fmt.Println(Match(ch1, ch2)) // true
//比較數(shù)組,注意!不能是切片
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約束類型的時候,發(fā)現(xiàn)它最后合并上了一個~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]
我們運行后,會發(fā)現(xiàn),第二個會報錯,因為MySliceInt允許的是int作為類型實參,而不是YourInt, 雖然YourInt類型底層類型是int,但它依舊不是int類型)。
這個時候~就排上用處了,我們可以這樣寫就可以了,表示底層的超集類型。
type IntAll interface {
~int | ~int64 | ~int32
}
六、泛型方法
接下來,我們來看下go中如何泛型方法,首先需要指出的是go里面的方法指的是接收器類型(receiver type),我們經(jīng)常會用這種方式來實現(xiàn)其他語言中類的作用。比如下面這個例子:
type DemoInt int
func (t DemoInt) methodName(param string) string {
}
我們看這種類型,不管是前面的(t DemoInt) 還是方法名后面參數(shù) (param string) 里面都會涉及到具體的類型變量,所以都可以改造成泛型。我們先來看下接收器(t DemoInt) 如何改照成泛型。
(一)接收器泛型
我們先定義1個泛型變量,然后在這個變量上加上1個方法,試著寫一下:
//申請一個自定義的泛型約束類型
type NumberAll interface {
~int|~int64|~int32|~int16|~int8|~float64|~float32
}
//申請一個泛型切片類型,泛型參數(shù)是T,約束的類型是 NumberAll
type 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,這樣之后,我們就完成了一個泛型接收器方法。
那么如何去調用呢?其實和普通的接收器方法是一樣的,只不過我們得先去實例化泛型切片。
//實例化成int
var ss1 SliceNumber[int] = []int{1, 2, 3, 4}
//簡化
//ss1 := SliceNumber[int]{1, 2, 34}
ss1.SumIntsOrFloats() // 10
//實例化成float64
var ss2 SliceNumber[float64] = []float64{1.11, 2.22, 3.33}
//簡化
//ss2 := SliceNumber[float64]{1.11, 2.22, 3.33}
ss2.SumIntsOrFloats() //6.66
這種泛型方法的運用,在實際上的很多場景都是很好用的的,比如不同類型的堆棧的入棧和出棧,這也是一個很經(jīng)典的例子:
//自定義一個類型約束
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
}
我們就可以傳入不同的類型數(shù)據(jù)去實例化調用一下:
//實例化成一個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()) //3
fmt.Println(s1.Pop()) //2
fmt.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.3
fmt.Println(s2.Pop()) //2.2
fmt.Println(s2.Pop()) //1.1
(二)方法的參數(shù)泛型
說完接收器泛型之后,我們來看下第二種泛型的方式,就是方法的參數(shù)泛型,就是接收器是一個普通的類型,在方法的參數(shù)里面來設置泛型變量。我們嘗試著寫一下:
type DemoSlice []int
func (d DemoSlice) FindOne[T int](a T) bool {
}
你會發(fā)現(xiàn),你方法里面的邏輯都還沒開始寫,編輯器就會標紅報錯了:Method cannot have type parameters。方法不能有類型參數(shù),即:方法是不支持泛型的。至少目前的1.18版本是不支持的??春罄m(xù)版本會不會支持的。
既然,函數(shù)是支持泛型的,接收器也是支持函數(shù)的,所以我們把他們結合起來,稍加改造:
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個是傳統(tǒng)的接口類型,1個是約束類型,法有啥區(qū)別呢?一個叫:方法集,另一個叫:類型集。其實他們本質上是一樣的。傳統(tǒng)的接口類型是,只要我實現(xiàn)了接口里面定義的方法,那我就是實現(xiàn)了這個接口。而約束類型,其實也是一樣,只要我傳入的值的類型,在你這約束范圍內,那就是符合要求的。
所以,go在1.18版本后,對 interface 的定義改了,改成了:
接口類型定義了一個類型集合。接口類型的變量可以存儲這個接口類型集合的任意一種類型的實例值。這種類型被稱之為實現(xiàn)了這個接口。接口類型的變量如果未初始化則它的值為nil。
那如果把這2者結合起來呢?
type MyError interface {
int | float64
Error() string
}
這種寫法看著好陌生,里面既有約束類型,又有方法,這是go1.18中新增的寫法,這種接口叫做:一般接口(General interface)。原先1.18之前的接口定義類型叫做:基本接口(Basic interfaces)。
所以總結一下:
-
如果,1個接口里面只有方法,也就是老的語法寫法,這個接口叫:基本接口。
-
如果,1個接口里面,有約束類型的,有或者沒有方法的,這個接口叫:一般接口。
不得不吐槽一下,這2個類型的名字取的真是有水準啊,跟沒有區(qū)別一樣,本身go里面的接口可以說是非常復雜的,這樣一個改動后,簡直是雪上加霜啊,直接把接口的難度等級又提升了一個等級。
(一)基本泛型接口
我們繼續(xù)看下如何定義一個泛型接口呢?它的寫法和泛型變量是類似的:
type MyInterface[T int | string] interface {
WriteOne(data T) T
ReadOne() T
}
接口名字后面接一個[],里面填充的接口里面方法中需要用到的泛型參數(shù)。這個和定義其他泛型變量是一致的。然后接口里面就是具體的空方法了,和泛型函數(shù)或者泛型方法的寫法一樣。
但是值得注意的是,別寫反了,別把泛型參數(shù)寫到了方法的層面,這樣是錯誤的語法:
//會提示錯誤:interface method must have no type parameters
type MyInterface interface {
WriteOne[T int | string] (data T) T
ReadOne[T int | string] () T
}
當我們定義好了上面這個泛型接口,因為里面只有方法,沒有約束類型的定義,所以它是個基本接口。那我們看下如何去實現(xiàn)這個基本泛型接口。
我們先定義1個普通的結構體類型,然后通過接收器方式綁定上2個方法:
type Note struct {
}
func (n Note) WriteOne(one string) string {
return "hello"
}
func (n Note) ReadOne() string {
return "small"
}
然后,我們看下如何實例化泛型接口,并且實現(xiàn)接口。這種寫法和普通的實現(xiàn)接口的方式是一直的,只不過要顯示的的傳入T的值是什么。
var one MyInterface[string] = Note{}
fmt.Println(one.WriteOne("hello"))
fmt.Println(one.ReadOne())
值得注意的是泛型參數(shù)的值的類型,要和被實現(xiàn)的方法的參數(shù)值要保證一致,不然會報錯:
//接口實例化用的是int,但是實現(xiàn)的方法里面都是string類型,并不匹配,無法被實現(xiàn)。
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
(二)一般泛型接口
我們現(xiàn)在再來定義一個一般泛型接口,也就是說接口里面,有約束類型??聪略趺磳懀?/span>
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,只能用作類型參數(shù)。
簡而言之, 一般泛型接口,只能被當做類型參數(shù)來使用,無法被實例化 。
type myInterface [T MyInterface2[int]] []T
但是這種這么變態(tài)的寫法,如何實例化呢?這個有待研究,反正至少沒報錯了。
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以及高性能服務上也有很深的了解。
推薦閱讀
我為大家整理了一份 從入門到進階的Go學習資料禮包 ,包含學習建議:入門看什么,進階看什么。 關注公眾號 「polarisxu」,回復? ebook ?獲?。贿€可以回復「進群」,和數(shù)萬 Gopher 交流學習。
