一文帶你了解Golang中的泛型
自從2022年 Golang 1.18 發(fā)布至今已有一年多了,在1.18版本中增加了非常重磅的一個(gè)功能,那就是泛型!Golang官方也對(duì)泛型格外重視:
“Generics are the biggest change we’ve made to Go since the first open source release”(泛型是自第一個(gè)開源版本以來我們對(duì) Go 所做的最大改變)
然而由于平時(shí)工作項(xiàng)目所用Go版本較為古老,一直沒有大范圍應(yīng)用泛型這個(gè)特性,雖然之前剛發(fā)布時(shí)就了解學(xué)習(xí)過,但是也有些模糊了,這里還是希望能夠記錄一下Golang泛型,以備后續(xù)參考,我講基于Golang官方文檔、博客、youtube演講等來進(jìn)行學(xué)習(xí)。
什么是泛型
泛型是一種可以編寫?yīng)毩⒂谑褂玫奶囟愋偷拇a的方法,可以通過編寫函數(shù)或類型來使用一組類型中的任何一個(gè)。泛型為Golang增添了三個(gè)重要功能:

- 函數(shù)和類型的類型參數(shù)
- 將接口類型定義為類型集,包括沒有方法的類型。也就是我們可以定義類型集和方法集
- 類型推斷,允許函數(shù)調(diào)用時(shí)省略類型參數(shù)
我們從類型參數(shù)開始逐步了解
類型參數(shù) Type parameters
類型參數(shù)讓我們可以參數(shù)化函數(shù)或者具有類型的類型,與普通的參數(shù)列表類似,類型參數(shù)使用方括號(hào)來表示
函數(shù)中使用類型參數(shù)
這里有一個(gè)常見的取最小值函數(shù),我們經(jīng)常會(huì)在代碼中寫(新版的Golang官方庫(kù)已經(jīng)支持了max以及min):
func Min(x, y float64) float64 {
if x < y {
return x
}
return y
}我們可以通過類型參數(shù)來替換 float64 類型來使這個(gè)函數(shù)更加通用,讓這個(gè)函數(shù)不僅僅適用于 float64 類型,可以這樣來做:
import "golang.org/x/exp/constraints"
func Min[T constraints.Ordered](x, y T) T {
if x < y {
return x
}
return y
}
m := Min[int](2, 3)在這里我們使用了類型參數(shù) T 來替換 float64 類型使得函數(shù)通用,由于 T 是一個(gè)新的類型,所以我們要在 [] 中聲明它。
函數(shù)定義好后,與普通的函數(shù)調(diào)用類似,我們需要傳入函數(shù)的實(shí)參以及創(chuàng)建一個(gè)接收值來接受函數(shù)實(shí)際返回的結(jié)果,不同的是,我們需要在 [] 傳入具體的類型值,向函數(shù)提供類型參數(shù) int 成為實(shí)例化。
實(shí)例化將會(huì)分兩步進(jìn)行:

- 編譯器將整個(gè)泛型函數(shù)或類型中的所有類型實(shí)參進(jìn)行替換
- 編譯器驗(yàn)證每個(gè)類型參數(shù)是否滿足了各自的約束
如果編譯器在第二步執(zhí)行失敗,實(shí)例化就會(huì)失敗且程序會(huì)fail。
我們也可以直接傳入類型參數(shù)來實(shí)例化函數(shù),而不需要傳入具體實(shí)參進(jìn)行實(shí)際調(diào)用,實(shí)例化過后我們就可以像普通函數(shù)調(diào)用一樣來調(diào)用這個(gè)實(shí)例化過后的函數(shù)了:
fmin := Min[float64] m := fmin(2.71, 3.14)
類型中使用類型參數(shù)
前面是一個(gè)函數(shù)中使用類型參數(shù)的例子,還有一個(gè)在類型中使用類型參數(shù)的例子:
type Tree[T interface{}] struct {
left, right *Tree[T]
value T
}
func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }
var stringTree Tree[string]這是一個(gè)通用的二叉樹類型定義,我們?cè)俅尾捎昧祟愋蛥?shù) T 來作為一個(gè)通用性的數(shù)據(jù)類型,這里定義了類型 Tree[T] 同時(shí)定義了它所具有的方法 Lookup(x T) *Tree[T]。
最后一行通過 var 對(duì)變量 stringTree 做了一次實(shí)例化,傳入?yún)?shù)類型為 string
類型集
func min(x,y float64)float64 func Gmin[T constraints.Ordered](x, y T) T
普通函數(shù) min() 每個(gè)參數(shù)值都有一個(gè)類型,例如min函數(shù)中,限定了 x,y 及 返回值只有在 float64 類型時(shí)才有效;而函數(shù) Gmin() 類型參數(shù)列表中每個(gè)類型參數(shù)都有一個(gè)類型,由于類型參數(shù)本身就是一種類型,因此類型參數(shù)的類型定義了類型集,這種元類型告訴了我們那些類型對(duì)該參數(shù)類型有效,因此這個(gè)元類型實(shí)際定義了類型集,我們可以稱之為類型約束。
在 Gmin() 中,類型越是是從約束包中導(dǎo)入的,這個(gè)包也是 Golang 標(biāo)準(zhǔn)庫(kù)中新增的包。這個(gè)Ordered約束描述了具有可排序值的所有類型的集合,或者換句話說,約束了能夠使用 < 運(yùn)算符(或 <= 、 > 等)進(jìn)行比較的類型范圍。所以只有具有可排序值的類型才能傳遞給GMin,在GMin函數(shù)體中,該類型參數(shù)的值可以用于與 < 等運(yùn)算符進(jìn)行比較。
接口類型定義為類型集 Type sets define by interface
在Golang中的類型約束必須是接口,接口類型可以用作值類型,也可以用作元類型。接口定義了方法,所以我們可以選擇需要存在某些方法的類型約束。但是在例子中的constraints.Ordered也是一個(gè)接口類型,并且 < 運(yùn)算符也并不是一個(gè)方法,那它是如何工作的呢?
在Golang的規(guī)范中,如果我們有一個(gè)接口定義了一組方法,帶有方法 a b c,那我們就可以說接口定義了一個(gè)帶有方法 a b c 的方法集,實(shí)現(xiàn)了這些方法的每個(gè)類型也就實(shí)現(xiàn)了該接口。如圖所示,類型 P Q R 都實(shí)現(xiàn)了接口

我們可以換個(gè)角度來看待這個(gè)規(guī)范,那就是接口定義了一組類型,這些類型都要具有接口中的方法。從這個(gè)角度來看,每個(gè)接口方法集都可以延伸出無限的類型,作為接口類型集元素的任何類型都實(shí)現(xiàn)該接口。要檢查某一類型是否實(shí)現(xiàn)了接口,僅需檢查該類型是否是該類型集的元素。

通過視角的轉(zhuǎn)換,就我們的目的而言,類型集視圖比方法集視圖有一個(gè)優(yōu)勢(shì):我們可以顯式地將類型添加到集合中,從而以新的方式控制類型集。
Golang通過對(duì)接口類型語(yǔ)法的擴(kuò)展來實(shí)現(xiàn)這一點(diǎn)。在下面的示例中,我們有一個(gè)具有int\string\bool三種類型的接口示例,并且該接口定義了這三種類型的集合

實(shí)際聲明的constraints.Ordered:
package constraints
type Ordered interface {
Integer|Float|~string
}這表明,Ordered接口是所有 int\float\string 類型的集合,豎線"|"表示這些類型的聯(lián)合(類型集)。Ordered接口沒有定義任何方法。"~"表示我們接受任意的基礎(chǔ)類型為string的類型集,包括類型string本身以及使用定義聲明的所有類型,例如type MyString string
我們?nèi)绻朐诮涌谥幸?guī)定方法,那么仍然是向后兼容的,在1.18版本后,接口可以像之前一樣包含方法或嵌入接口,同時(shí)我們也可以嵌入非接口類型、聯(lián)合和底層類型集。
用作約束的接口可以指定名稱(例如Order),也可以是內(nèi)聯(lián)在類型參數(shù)列表中的接口,例如:
[S interface{~[]E}, E interface{}]這里類型參數(shù)列表中有兩個(gè)類型參數(shù),分別是 S 和 E,S定義的是一個(gè)切片類型的類型參數(shù),其元素類型可以是任何類型,由于 interface{} 在約束時(shí)可以省略封裝,因此可以簡(jiǎn)單寫為:
[S ~[]E, E interface{}]
// Go1.18中引入的any即為interface{}: interface{} => any
[S ~[]E, E any]作為類型集的接口是一種新機(jī)制,是Go中類型約束的關(guān)鍵。
類型推斷 type inference
函數(shù)參數(shù)類型推斷
有了類型參數(shù),就需要傳遞類型參數(shù),這可能會(huì)導(dǎo)致代碼冗長(zhǎng)
func GMin[T constraints.Ordered](x, y T) T { ... }
var a, b, m float64
m = GMin[float64](a, b) // explicit type argument這里我們指定了實(shí)例化的參數(shù)類型 float64,實(shí)際上,編譯器可以從實(shí)參推斷出類型參數(shù),這樣可以使代碼更為簡(jiǎn)潔清晰:
var a, b, m float64 m = GMin(a, b) // no type argument
這種從函數(shù)參數(shù)的類型推斷出參數(shù)類型的推斷稱為函數(shù)參數(shù)類型推斷。如果推斷成功,函數(shù)就和普通函數(shù)一樣正常使用,如果不成功,編譯器會(huì)報(bào)錯(cuò),我們還是需要指定類型。
函數(shù)參數(shù)類型推斷僅適用于在函數(shù)參數(shù)中使用的類型參數(shù),不適用于在函數(shù)結(jié)果或僅在函數(shù)體中使用的類型參數(shù)。
約束類型推斷
從一個(gè)整數(shù)切片的縮放示例看起:
// Scale returns a copy of s with each element multiplied by c.
// This implementation has a problem, as we will see.
func Scale[E constraints.Integer](s []E, c E) []E {
r := make([]E, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}這個(gè)函數(shù)適用于所有整數(shù)類型的切片,看起來和之前的沒有什么不同,但是存在一個(gè)問題,讓我們繼續(xù)揭開這個(gè)問題:
假設(shè)我們有一個(gè) Point 類型,它是一個(gè) int32 類型元素的切片,同時(shí)他具有自己的一個(gè)方法 String():
type Point []int32
func (p Point) String() string {
// Details not important.
}這時(shí)候我們想要去放大 Point 為2倍,我們可以調(diào)用 Scale 函數(shù):
// ScaleAndPrint doubles a Point and prints it.
func ScaleAndPrint(p Point) {
r := Scale(p, 2)
fmt.Println(r.String()) // DOES NOT COMPILE
}看起來沒有什么問題,但是編譯就會(huì)報(bào)錯(cuò)。
問題在于,調(diào)用 Scale() 函數(shù)后,會(huì)返回一個(gè) []E 類型的值,由于傳入的實(shí)參類型為 int32,因此函數(shù)的實(shí)際返回值為 []int32 而不是 Point,而 []int32 類型不具有其他方法。為了解決這個(gè)問題,我們要這樣做:
// Scale returns a copy of s with each element multiplied by c.
func Scale[S ~[]E, E constraints.Integer](s S, c E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = v * c
}
return r
}通過引入新的參數(shù)類型 S 及約束,使得返回值類型為 S 而不是 []E,函數(shù)唯一的變化為,在實(shí)際調(diào)用時(shí)傳遞 S 而不是 []E,同樣返回值會(huì)返回 type Point。
這里也就引出了約束類型推斷的概念,也就是說,我們無需通過指定約束類型來實(shí)例化調(diào)用函數(shù),類似于:
r := Scale[Point, int32](p, 2)
編譯器可以推斷出類型參數(shù) S 是 Point。但是該函數(shù)還有一個(gè)類型參數(shù)E,實(shí)參為 2,2是一個(gè)無類型的常量,因此, 編譯器推斷出 E 的類型實(shí)參是切片的元素類型過程就是我們所說的約束類型推斷
通常情況當(dāng)一個(gè)約束使用某種類型的形式時(shí) ,其中該類型是使用其他類型參數(shù)編寫的,會(huì)用到這種應(yīng)用場(chǎng)景
官方建議的泛型使用時(shí)機(jī)
適用場(chǎng)景:
適用于任何元素類型的切片、映射和通道的函數(shù)
通過通用數(shù)據(jù)結(jié)構(gòu)用于通用數(shù)據(jù)(非接口類型,省去斷言)
當(dāng)不同類型實(shí)現(xiàn)通用方法并且不同類型的方法實(shí)現(xiàn)看起來都一樣時(shí)
非適用場(chǎng)景:
僅在類型參數(shù)上調(diào)用方法時(shí)(例如io.reader)
當(dāng)每種類型的公共方法的實(shí)現(xiàn)不同時(shí) 當(dāng)對(duì)不同參數(shù)具有不同操作時(shí)
到此這篇關(guān)于一文帶你了解Golang中的泛型的文章就介紹到這了,更多相關(guān)Golang泛型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang語(yǔ)言的多種變量聲明方式與使用場(chǎng)景詳解
Golang當(dāng)中的變量類型和C/C++比較接近,一般用的比較多的也就是int,float和字符串,下面這篇文章主要給大家介紹了關(guān)于Golang語(yǔ)言的多種變量聲明方式與使用場(chǎng)景的相關(guān)資料,需要的朋友可以參考下2022-02-02
Go語(yǔ)言并發(fā)之context標(biāo)準(zhǔn)庫(kù)的使用詳解
Context的出現(xiàn)是為了解決在大型應(yīng)用程序中的并發(fā)環(huán)境下,協(xié)調(diào)和管理多個(gè)goroutine之間的通信、超時(shí)和取消操作的問題,本文就來和大家簡(jiǎn)單聊聊它的具體用法,希望對(duì)大家有所幫助2023-06-06
Golang實(shí)現(xiàn)定時(shí)任務(wù)的幾種方法小結(jié)
在 Golang 開發(fā)中,定時(shí)任務(wù)是常見的需求,本文將介紹幾種在 Golang 中實(shí)現(xiàn)定時(shí)任務(wù)的方法,包括 time 包的定時(shí)器、ticker,以及第三方庫(kù) cron,并通過示例代碼展示它們的使用方式,需要的朋友可以參考下2024-01-01
RabbitMQ延時(shí)消息隊(duì)列在golang中的使用詳解
延時(shí)隊(duì)列常使用在某些業(yè)務(wù)場(chǎng)景,使用延時(shí)隊(duì)列可以簡(jiǎn)化系統(tǒng)的設(shè)計(jì)和開發(fā)、提高系統(tǒng)的可靠性和可用性、提高系統(tǒng)的性能,下面我們就來看看如何在golang中使用RabbitMQ的延時(shí)消息隊(duì)列吧2023-11-11
Golang實(shí)現(xiàn)根據(jù)某個(gè)特定字段對(duì)結(jié)構(gòu)體的順序進(jìn)行排序
這篇文章主要為大家詳細(xì)介紹了Golang如何實(shí)現(xiàn)根據(jù)某個(gè)特定字段對(duì)結(jié)構(gòu)體的順序進(jìn)行排序,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03
Golang設(shè)計(jì)模式之原型模式詳細(xì)講解
如果一個(gè)類的有非常多的屬性,層級(jí)還很深。每次構(gòu)造起來,不管是直接構(gòu)造還是用建造者模式,都要對(duì)太多屬性進(jìn)行復(fù)制,那么有沒有一種好的方式讓我們創(chuàng)建太的時(shí)候使用體驗(yàn)更好一點(diǎn)呢? 今天的文章里就給大家介紹一種設(shè)計(jì)模式,來解決這個(gè)問題2023-01-01
Go到底能不能實(shí)現(xiàn)安全的雙檢鎖(推薦)
這篇文章主要介紹了Go到底能不能實(shí)現(xiàn)安全的雙檢鎖,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05

