Go 泛型簡明入門教程
閱讀本文大概需要 10?分鐘。
大家好,我是 polarisxu。
有泛型的 Go 版本 1.18 已經(jīng)發(fā)布了 Beta1 版本,之前陸陸續(xù)續(xù)介紹了泛型,但可能有些人還是對 Go 泛型沒有完整的了解,因此有這份入門教程。
01 準(zhǔn)備工作
開始學(xué)習(xí)泛型之前,你應(yīng)該安裝 Go1.18 Beta1 或之后發(fā)布的版本,建議使用 goup 等版本管理工具,當(dāng)然也可以直接通過 playground 來驗(yàn)證:https://go.dev/play/?v=gotip。
不過,本教程基于本地安裝 Go1.18 Beta1 為例進(jìn)行。
$?goup?install?1.18beta1
Downloaded???0.0%?(????16384?/?143162528?bytes)?...
Downloaded???5.9%?(??8404928?/?143162528?bytes)?...
Downloaded??14.1%?(?20234096?/?143162528?bytes)?...
Downloaded??22.3%?(?31981328?/?143162528?bytes)?...
Downloaded??30.5%?(?43695808?/?143162528?bytes)?...
Downloaded??38.7%?(?55443040?/?143162528?bytes)?...
Downloaded??45.7%?(?65486352?/?143162528?bytes)?...
Downloaded??53.9%?(?77200832?/?143162528?bytes)?...
Downloaded??62.1%?(?88866144?/?143162528?bytes)?...
Downloaded??70.3%?(100580624?/?143162528?bytes)?...
Downloaded??78.4%?(112295088?/?143162528?bytes)?...
Downloaded??85.5%?(122371168?/?143162528?bytes)?...
Downloaded??93.7%?(134102032?/?143162528?bytes)?...
Downloaded?100.0%?(143162528?/?143162528?bytes)
INFO[0013]?Unpacking?/Users/xuxinhua/.go/go1.18beta1/go1.18beta1.darwin-amd64.tar.gz?...
INFO[0020]?Success:?go1.18beta1?downloaded?in?/Users/xuxinhua/.go/go1.18beta1
INFO[0020]?Default?Go?is?set?to?'go1.18beta1'
驗(yàn)證是否安裝成功:
$?go?version
go?version?go1.18beta1?darwin/amd64
02 創(chuàng)建項(xiàng)目
切換到 $HOME 目錄,Linux/Mac 執(zhí)行:
$?cd?~
Windows 下執(zhí)行(在 C 盤,基于 cmd 或 PowerShell):
C:\>?cd?%HOMEPATH%
然后創(chuàng)建目錄并初始化模塊:
$?mkdir?generics
$?cd?generics
$?go?mod?init?github.com/polaris1119/generics
go:?creating?new?go.mod:?module?github.com/polaris1119/generics
其中的模塊前綴可以替換為你喜歡的名字。
03 添加非泛型函數(shù)
下面以 map 為例,先看非泛型如何處理,泛型又是如何處理。
假如有兩個 map,分別是 map[string]int 和 map[string]float64,編寫函數(shù)將 map 中的 value 值相加,返回結(jié)果。因?yàn)橛袃蓚€類型,因此編寫兩個函數(shù)。
func?SumInts(m?map[string]int)?int?{
????var?s?int
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
func?SumFloats(m?map[string]float64)?float64?{
????var?s?float64
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
在 main 函數(shù)中初始化兩個 map 并調(diào)用上面的函數(shù)。
func?main()?{
????ints?:=?map[string]int{
????????"first":??34,
????????"second":?12,
????}
????floats?:=?map[string]float64{
????????"first":??35.98,
????????"second":?26.99,
????}
????fmt.Printf("非泛型計(jì)算結(jié)果,SumInts:?%v,?SumFloats:?%v\n",?SumInts(ints),?SumFloats(floats))
}
運(yùn)行后,輸出結(jié)果:
$?go?run?main.go
非泛型計(jì)算結(jié)果,SumInts:?46,?SumFloats:?62.97
雖然得到了想要的結(jié)果,但 SumInts 和 SumFloats 的邏輯差不多。如果將來有其他類型,我們必須增加額外的函數(shù),代碼邏輯也類似。
有了泛型,只需要一個函數(shù)就可以實(shí)現(xiàn)以上兩個函數(shù)的功能,而且可以方便擴(kuò)展為支持其他相關(guān)類型,比如 map[iint]float64 等。
03 泛型處理多類型
要支持任一類型的值,該函數(shù)將需要一種方法來聲明它支持的類型。同時,調(diào)用者需要一種方法來指定它是使用整數(shù) map 還是浮點(diǎn)數(shù) map 進(jìn)行調(diào)用,即調(diào)用時指定實(shí)際參數(shù)的類型。
為了支持這一點(diǎn),需要編寫一個函數(shù),除了它的普通函數(shù)參數(shù)外,還需要聲明類型參數(shù)。這些類型參數(shù)使函數(shù)具有通用性,使其能夠處理不同類型的參數(shù)。這樣,你可以使用類型參數(shù)和普通函數(shù)參數(shù)調(diào)用該通用函數(shù),即泛型函數(shù)。
每個類型參數(shù)都有一個類型約束,作為類型參數(shù)的一種元類型。每個類型約束指定調(diào)用代碼可以用于相應(yīng)類型參數(shù)的允許類型。
雖然類型參數(shù)的約束通常表示一組類型,但在編譯時類型參數(shù)代表單個類型——調(diào)用代碼作為類型參數(shù)提供的類型。如果類型參數(shù)的約束不允許該調(diào)用者指定的類型,則代碼將無法編譯。
請記住,類型參數(shù)必須支持泛型代碼對其執(zhí)行的所有操作。例如,函數(shù)對參數(shù)執(zhí)行加減運(yùn)算,而 string 是不支持的,因此約束中不能包含 string 類型,否則代碼將無法編譯。
如果沒看懂,就看具體代碼:
func?SumIntsOrFloats[K?comparable,?V?int?|?float64](m?map[K]V)?V?{
????var?s?V
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
函數(shù) SumIntsOrFloats 聲明了兩種參數(shù):類型參數(shù)和普通函數(shù)參數(shù)。其中類型參數(shù)放在 [] 中,普通參數(shù)依然放在 () 中。(別問類型參數(shù)為什么不用 <>,官方給了解釋:https://groups.google.com/g/golang-nuts/c/7t-Q2vt60J8/m/65D5xBDvBgAJ)
該函數(shù)的類型參數(shù)是:K comparable, V int | float64,其中 K、V 的名字不重要,分別表示某種類型,comparable 和 int | float64 是 K、V 類型的約束,即調(diào)用該方法時,K、V 允許的類型。comparable 是語言預(yù)定義的約束,官方的解釋如下:https://pkg.go.dev/builtin@master#comparable
comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, interfaces, 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.
即表示所有可比較類型,也就是說,K 可以是任意可比較類型。
而 V 的類型約束 int | float64 表示只允許是 int 或 float64,其他類型編譯會報錯。關(guān)于類型約束更多內(nèi)容,可以參考我之前寫的文章:Go1.18 類型約束那些事。
再看該函數(shù)的普通參數(shù):m map[K]V,這表明,m 是一個 map,它的 key 類型是 K,value 類型是 V。很顯然,這兩個是該函數(shù)「類型參數(shù)」定義的類型。
泛型函數(shù)有了,該如何調(diào)用呢?
在 main 中增加如下調(diào)用:
fmt.Printf("泛型計(jì)算結(jié)果,Ints?結(jié)果:?Floats?結(jié)果:?%v\n",?SumIntsOrFloats[string,?int](ints),?SumIntsOrFloats[string,?float64](floats))
同一個函數(shù),支持 map[string]int 和 map[string]float64。
注意,我們在調(diào)用函數(shù)和聲明函數(shù)類型,用 [] 指定了具體的類型,比如 SumIntsOrFloats[string, int](ints),即調(diào)用時普通參數(shù)是什么類型通過 [] 指定。很顯然,這很繁瑣,實(shí)際上 Go 會進(jìn)行類型推斷,即編譯器會通過普通參數(shù)的類型推導(dǎo)出「類型參數(shù)」。不過,跟 Go 中其他類型自動推導(dǎo)類似,有些情況是無法自動推導(dǎo)的,這時候必須手動指定實(shí)際的類型參數(shù)。
因此,上面的調(diào)用代碼也可以簡寫為:
fmt.Printf("泛型計(jì)算結(jié)果,Ints?結(jié)果:?Floats?結(jié)果:?%v\n",?SumIntsOrFloats(ints),?SumIntsOrFloats(floats))
運(yùn)行后,得到如下結(jié)果:
$?go?run?main.go
非泛型計(jì)算結(jié)果,SumInts:?46,?SumFloats:?62.97
泛型計(jì)算結(jié)果,Ints?結(jié)果:?46,?Floats?結(jié)果:?62.97
04 聲明類型約束
上文已經(jīng)大概解釋了類型約束,針對本文例子,解釋下類型約束。
上面沒有將 int | float64 定義為一個命名約束,相當(dāng)于約束字面量(或聯(lián)合類型)。一般有兩種場景會單獨(dú)聲明類型約束:
約束太長,比如有很多類型,直接寫在函數(shù)中,會嚴(yán)重影響可讀性 方便類型約束重用
將上面 V 的約束定義為單獨(dú)的類型約束:(實(shí)際是接口,但不能作為單獨(dú)類型使用)
type?Number?interface{
??int?|?float64
}
基于此定義另外一個函數(shù) SumNumbers:
func?SumNumbers[K?comparable,?V?Number](m?map[K]V)?V?{
????var?s?V
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
類似的,可以這樣調(diào)用(省略了「類型參數(shù)」):
fmt.Printf("泛型計(jì)算結(jié)果(帶?Constraint),Ints?結(jié)果:?%v,?Floats?結(jié)果:?%v\n",
????SumNumbers(ints),
????SumNumbers(floats))
05 總結(jié)
泛型的內(nèi)容遠(yuǎn)不止這些,但本文作為入門教程,旨在介紹基礎(chǔ)內(nèi)容,讓大家對泛型使用有一個基本了解。本文的示例參照官方泛型教程:https://go.dev/doc/tutorial/generics。
本文完整代碼見 playground:https://go.dev/play/p/TwS6wda3nbv?v=gotip。
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
