『每周譯Go』Go 官方出品泛型教程:如何開始使用泛型
備注:這是一個(gè)beta 版本的內(nèi)容
這個(gè)教程介紹了Go泛型的基礎(chǔ)概念。通過泛型,你可以聲明并使用函數(shù)或者是類型,那些用于調(diào)用代碼時(shí)參數(shù)需要兼容多個(gè)不同類型的情況。
在這個(gè)教程里,你會(huì)聲明兩個(gè)普通的函數(shù),然后復(fù)制一份相同的邏輯到一個(gè)泛型的方法里。
你會(huì)通過以下幾個(gè)章節(jié)來進(jìn)行學(xué)習(xí):
為你的代碼創(chuàng)建一個(gè)文件夾; 添加非泛型函數(shù); 添加一個(gè)泛型函數(shù)來處理多種類型; 在調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù); 聲明一個(gè)類型約束。
備注:對(duì)其他教程,可以查看教程(https://gocn.vip/topics/20885)?
備注:你同時(shí)也可以使用 “Go dev branch”(https://gocn.vip/topics/20885)模式來編輯和運(yùn)行你的代碼,如果你更喜歡以這種形式的話

安裝Go 1.18 Beta 1或者更新的版本。對(duì)安裝流程,請(qǐng)看安裝并使用beta版本。 代碼編輯器。任何你順手的代碼編輯器。 一個(gè)命令終端。Go在任何終端工具上都很好使用,比如Linux 、Mac、PowerShell或者Windows上的cmd。
安裝并使用beta版本
這個(gè)教程需要Beta 1的泛型特性。安裝beta版本,需要通過下面幾個(gè)步驟:
1、 執(zhí)行下面的指令安裝beta版本
?$?go?install?golang.org/dl/go1.18beta1@latest
2、 執(zhí)行下面的指令下載更新
?$?go1.18beta1?download
3、用beta版本執(zhí)行g(shù)o命令,而不是Go的發(fā)布版本(如果你本地有安裝的話)
你可以使用beta版本名稱或者把beta重命名成別的名稱來執(zhí)行命令。
使用beta版本名稱,你可以通過go1.18beta1來執(zhí)行指令而不是go:
$?go1.18beta1?version
通過對(duì)beta版本名稱重命名,你可以簡(jiǎn)化指令:
shell
$ alias go=go1.18beta1
$ go version
在這個(gè)教程中將假設(shè)你已經(jīng)對(duì)beta版本名稱進(jìn)行了重命名。

在一開始,先給你要寫的代碼創(chuàng)建一個(gè)文件夾
1、 打開一個(gè)命令提示符并切換到/home文件夾
在Linux或者M(jìn)ac上:
$?cd
在windows上:
?C:\>?cd?%HOMEPATH%
在接下去的教程里面會(huì)用$來代表提示符。指令在windows上也適用。
2、 在命令提示符下,為你的代碼創(chuàng)建一個(gè)名為generics的目錄
?$?mkdir?generics
???$?cd?generics
3、 創(chuàng)建一個(gè)module來存放你的代碼
執(zhí)行go mod init指令,參數(shù)為你新代碼的module路徑
?$?go?mod?init?example/generics
???go:?creating?new?go.mod:?module?example/generics
備注:對(duì)生產(chǎn)環(huán)境,你會(huì)指定一個(gè)更符合你自己需求的module路徑。更多的請(qǐng)看依賴管理(https://go.dev/doc/modules/managing-dependencies)
接下來,你會(huì)增加一些簡(jiǎn)單的和maps相關(guān)的代碼。

在這一步中,你將添加兩個(gè)函數(shù),每個(gè)函數(shù)都會(huì)累加 map 中的值 ,并返回總和。
你將聲明兩個(gè)函數(shù)而不是一個(gè),因?yàn)槟阋幚韮煞N不同類型的map:一個(gè)存儲(chǔ)int64類型的值,另一個(gè)存儲(chǔ)float64類型的值。
寫代碼
1、 用你的文本編輯器,在generics文件夾里面創(chuàng)建一個(gè)叫main.go的文件。你將會(huì)在這個(gè)文件內(nèi)寫你的Go代碼。
2、 到main.go文件的上方,粘貼如下的包的聲明。
?package?main
一個(gè)獨(dú)立的程序(相對(duì)于一個(gè)庫(kù))總是在main包中。
3、 在包的聲明下面,粘貼以下兩個(gè)函數(shù)的聲明。
?//?SumInts?adds?together?the?values?of?m.
???func?SumInts(m?map[string]int64)?int64?{
???????var?s?int64
???????for?_,?v?:=?range?m?{
???????????s?+=?v
???????}
???????return?s
???}
???
???//?SumFloats?adds?together?the?values?of?m.
???func?SumFloats(m?map[string]float64)?float64?{
???????var?s?float64
???????for?_,?v?:=?range?m?{
???????????s?+=?v
???????}
???????return?s
???}
在這段代碼中,你:
聲明兩個(gè)函數(shù),將一個(gè)map的值加在一起,并返回總和。 SumFloats接收一個(gè)map,key為string類型,value為floa64類型。 SumInt接收一個(gè)map,key為string類型,value為int64類型。
func?main()?{
???//?Initialize?a?map?for?the?integer?values
???ints?:=?map[string]int64{
???????"first":?34,
???????"second":?12,
???}
???
???//?Initialize?a?map?for?the?float?values
???floats?:=?map[string]float64{
???????"first":?35.98,
???????"second":?26.99,
???}
???
???fmt.Printf("Non-Generic?Sums:?%v?and?%v\n",
???????SumInts(ints),
???????SumFloats(floats))
???}
在這段代碼中,你:
初始化一個(gè)key為string,value為float64的map和一個(gè)key為string,value為int64的map,各有2條數(shù)據(jù); 調(diào)用之前聲明的兩個(gè)方法來獲取每個(gè)map的值的總和; 打印結(jié)果。
5、 靠近main.go頂部,僅在包聲明的下方,導(dǎo)入你剛剛寫的代碼所需要引用的包。
第一行代碼應(yīng)該看起來如下所示:
package?main
???import?"fmt"
6、 保存main.go.
運(yùn)行代碼
在main.go所在目錄下,通過命令行運(yùn)行代碼
$?go?run?.
Non-Generic?Sums:?46?and?62.97
有了泛型,你可以只寫一個(gè)函數(shù)而不是兩個(gè)。接下來,你將為maps添加一個(gè)泛型函數(shù),來允許接收整數(shù)類型或者是浮點(diǎn)數(shù)類型。

在這一節(jié),你將會(huì)添加一個(gè)泛型函數(shù)來接收一個(gè)map,可能值是整數(shù)類型或者浮點(diǎn)數(shù)類型的map,有效地用一個(gè)函數(shù)替換掉你剛才寫的2個(gè)函數(shù)。
為了支持不同類型的值,這個(gè)函數(shù)需要有一個(gè)方法來聲明它所支持的類型。另一方面,調(diào)用代碼將需要一種方法來指定它是用整數(shù)還是浮點(diǎn)數(shù)來調(diào)用。
為了實(shí)現(xiàn)上面的描述,你將會(huì)聲明一個(gè)除了有普通函數(shù)參數(shù),還有類型參數(shù)的函數(shù)。這個(gè)類型參數(shù)實(shí)現(xiàn)了函數(shù)的通用性,使得它可以處理多個(gè)不同的類型。你將會(huì)用類型參數(shù)和普通函數(shù)參數(shù)來調(diào)用這個(gè)泛型函數(shù)。
每個(gè)類型參數(shù)都有一個(gè)類型約束,類似于每個(gè)類型參數(shù)的meta-type。每個(gè)類型約束都指定了調(diào)用代碼時(shí)每個(gè)對(duì)應(yīng)輸入?yún)?shù)的可允許的類型。
雖然類型參數(shù)的約束通常代表某些類型,但是在編譯的時(shí)候類型參數(shù)只代表一個(gè)類型-在調(diào)用代碼時(shí)作為類型參數(shù)。如果類型參數(shù)的類型不被類型參數(shù)的約束所允許,代碼則無法編譯。
需要記住的是類型參數(shù)必須滿足泛型代碼對(duì)它的所有的操作。舉個(gè)例子,如果你的代碼嘗試去做一些string的操作(比如索引),而這個(gè)類型參數(shù)包含數(shù)字的類型,那代碼是無法編譯的。
在下面你要編寫的代碼里,你會(huì)使用允許整數(shù)或者浮點(diǎn)數(shù)類型的限制。
寫代碼?
1、 在你之前寫的兩個(gè)函數(shù)的下方,粘貼下面的泛型函數(shù)
?//?SumIntsOrFloats?sums?the?values?of?map?m.?It?supports?both?int64?and?float64
???//?as?types?for?map?values.
???func?SumIntsOrFloats[K?comparable,?V?int64?|?float64](m?map[K]V)?V?{
???????var?s?V
???????for?_,?v?:=?range?m?{
???????????s?+=?v
???????}
???????return?s
???}
在這段代碼里,你:
聲明了一個(gè)帶有2個(gè)類型參數(shù)(方括號(hào)內(nèi))的SumIntsOrFloats函數(shù),K和V,一個(gè)使用類型參數(shù)的參數(shù),類型為map[K]V的參數(shù)m。 為K類型參數(shù)指定可比較的類型約束。事實(shí)上,針對(duì)此類情況,在Go里面可比較的限制是會(huì)提前聲明。它允許任何類型的值可以作為比較運(yùn)算符==和!=的操作符。在Go里面,map的key是需要可比較的。因此,將K聲明為可比較的是很有必要的,這樣你就可以使用K作為map變量的key。這樣也保證了調(diào)用代碼方使用一個(gè)被允許的類型做map的key。 為V類型參數(shù)指定一個(gè)兩個(gè)類型合集的類型約束:int64和float64。使用|指定了2個(gè)類型的合集,表示約束允許這兩種類型。任何一個(gè)類型都會(huì)被編譯器認(rèn)定為合法的傳參參數(shù)。 指定參數(shù)m為類型map[K]V,其中K和V的類型已經(jīng)指定為類型參數(shù)。注意到因?yàn)镵是可比較的類型,所以map[K]V是一個(gè)有效的map類型。如果我們沒有聲明K是可比較的,那么編譯器會(huì)拒絕對(duì)map[K]V的引用。
?fmt.Printf("Generic?Sums:?%v?and?%v\n",
???????SumIntsOrFloats[string,?int64](ints),
???????SumIntsOrFloats[string,?float64](floats))
在這段代碼里,你:
調(diào)用你剛才聲明的泛型函數(shù),傳遞你創(chuàng)建的每個(gè)map。
指定類型參數(shù)-在方括號(hào)內(nèi)的類型名稱-來明確你所調(diào)用的函數(shù)中應(yīng)該用哪些類型來替代類型參數(shù)。
你將會(huì)在下一節(jié)看到,你通??梢栽诤瘮?shù)調(diào)用時(shí)省略類型參數(shù)。Go通常可以從代碼里推斷出來。
打印函數(shù)返回的總和。
運(yùn)行代碼
在main.go所在目錄下,通過命令行運(yùn)行代碼
$?go?run?.
Non-Generic?Sums:?46?and?62.97
Generic?Sums:?46?and?62.97
為了運(yùn)行你的代碼,在每次調(diào)用的時(shí)候,編譯器都會(huì)用該調(diào)用中指定的具體類型替換類型參數(shù)。
在調(diào)用你寫的泛型函數(shù)時(shí),你指定了類型參數(shù)來告訴編譯器用什么類型來替換函數(shù)的類型參數(shù)。正如你將在下一節(jié)所看到的,在許多情況下,你可以省略這些類型參數(shù),因?yàn)榫幾g器可以推斷出它們。

在這一節(jié),你會(huì)添加一個(gè)泛型函數(shù)調(diào)用的修改版本,通過一個(gè)小的改變來簡(jiǎn)化代碼。在這個(gè)例子里你將移除掉不需要的類型參數(shù)。
當(dāng)Go編譯器可以推斷出你要使用的類型時(shí),你可以在調(diào)用代碼中省略類型參數(shù)。編譯器從函數(shù)參數(shù)的類型中推斷出類型參數(shù)。
注意這不是每次都可行的。舉個(gè)例子,如果你需要調(diào)用一個(gè)沒有參數(shù)的泛型函數(shù),那么你需要在調(diào)用函數(shù)時(shí)帶上類型參數(shù)。
寫代碼
在main.go的代碼下方,粘貼下面的代碼。
fmt.Printf("Generic?Sums,?type?parameters?inferred:?%v?and?%v\n",
??????SumIntsOrFloats(ints),
??????SumIntsOrFloats(floats))
在這段代碼里,你:
調(diào)用泛型函數(shù),省略類型參數(shù)。
運(yùn)行代碼
在main.go所在目錄下,通過命令行運(yùn)行代碼
$?go?run?.
Non-Generic?Sums:?46?and?62.97
Generic?Sums:?46?and?62.97
Generic?Sums,?type?parameters?inferred:?46?and?62.97
接下來,你將通過把整數(shù)和浮點(diǎn)數(shù)的合集定義到一個(gè)你可以重復(fù)使用的類型約束中,比如從其他的代碼,來進(jìn)一步簡(jiǎn)化這個(gè)函數(shù)。

在最后一節(jié)中,你將把你先前定義的約束移到它自己的interface中,這樣你就可以在多個(gè)地方重復(fù)使用它。以這種方式聲明約束有助于簡(jiǎn)化代碼,尤其當(dāng)一個(gè)約束越來越復(fù)雜的時(shí)候。
你將類型參數(shù)定義為一個(gè)interface。約束允許任何類型實(shí)現(xiàn)這個(gè)interface。舉個(gè)例子,如果你定義了一個(gè)有三個(gè)方法的類型參數(shù)interface,然后用它作為一個(gè)泛型函數(shù)的類型參數(shù),那么調(diào)用這個(gè)函數(shù)的類型參數(shù)必須實(shí)現(xiàn)這些方法。
你將在本節(jié)中看到,約束interface也可以指代特定的類型。
寫代碼
1、 在main函數(shù)上面,緊接著import下方,粘貼如下代碼來定義類型約束。
type?Number?interface?{
???????int64?|?float64
???}
在這段代碼里,你:
聲明一個(gè)Number interface類型作為類型限制
在interface內(nèi)聲明int64和float64的合集
本質(zhì)上,你是在把函數(shù)聲明中的合集移到一個(gè)新的類型約束中。這樣子,當(dāng)你想要約束一個(gè)類型參數(shù)為int64或者float64,你可以使用Number interface而不是寫 int64 | float64。
2、 在你已寫好的函數(shù)下方,粘貼如下泛型函數(shù),SumNumbers。
?//?SumNumbers?sums?the?values?of?map?m.?Its?supports?both?integers
???//?and?floats?as?map?values.
???func?SumNumbers[K?comparable,?V?Number](m?map[K]V)?V?{
???????var?s?V
???????for?_,?v?:=?range?m?{
???????????s?+=?v
???????}
???????return?s
???}
在這段代碼,你:
聲明一個(gè)泛型函數(shù),其邏輯與你之前聲明的泛型函數(shù)相同,但是是使用新的interface類型作為類型參數(shù)而不是合集。和之前一樣,你使用類型參數(shù)作為參數(shù)和返回類型。
?fmt.Printf("Generic?Sums?with?Constraint:?%v?and?%v\n",
???????SumNumbers(ints),
???????SumNumbers(floats))
在這段代碼里,你:
每個(gè)map依次調(diào)用SumNumbers,并打印數(shù)值的總和。 與上一節(jié)一樣,你可以在調(diào)用泛型函數(shù)時(shí)省略類型參數(shù)(方括號(hào)中的類型名稱)。Go編譯器可以從其他參數(shù)中推斷出類型參數(shù)。
運(yùn)行代碼
在main.go所在目錄下,通過命令行運(yùn)行代碼
$?go?run?.
Non-Generic?Sums:?46?and?62.97
Generic?Sums:?46?and?62.97
Generic?Sums,?type?parameters?inferred:?46?and?62.97
Generic?Sums?with?Constraint:?46?and?62.97

完美結(jié)束!你剛才已經(jīng)給你自己介紹了Go的泛型。
如果你想繼續(xù)試驗(yàn),你可以嘗試用整數(shù)約束和浮點(diǎn)數(shù)約束來寫Number interface,來允許更多的數(shù)字類型。
建議閱讀的相關(guān)文章:
Go Tour 是一個(gè)很好的,手把手教Go基礎(chǔ)的介紹。 你可以在 Effective Go(https://go.dev/doc/effective_go) 和 How to write Go code(https://go.dev/doc/code) 中找到非常實(shí)用的GO的練習(xí)。

你可以在Go playground運(yùn)行這個(gè)代碼。在playground只需要點(diǎn)擊Run按鈕即可。
package?main
import?"fmt"
type?Number?interface?{
????int64?|?float64
}
func?main()?{
????//?Initialize?a?map?for?the?integer?values
????ints?:=?map[string]int64{
????????"first":?34,
????????"second":?12,
????}
????//?Initialize?a?map?for?the?float?values
????floats?:=?map[string]float64{
????????"first":?35.98,
????????"second":?26.99,
????}
????fmt.Printf("Non-Generic?Sums:?%v?and?%v\n",
????????SumInts(ints),
????????SumFloats(floats))
????fmt.Printf("Generic?Sums:?%v?and?%v\n",
????????SumIntsOrFloats[string,?int64](ints),
????????SumIntsOrFloats[string,?float64](floats))
????fmt.Printf("Generic?Sums,?type?parameters?inferred:?%v?and?%v\n",
????????SumIntsOrFloats(ints),
????????SumIntsOrFloats(floats))
????fmt.Printf("Generic?Sums?with?Constraint:?%v?and?%v\n",
????????SumNumbers(ints),
????????SumNumbers(floats))
}
//?SumInts?adds?together?the?values?of?m.
func?SumInts(m?map[string]int64)?int64?{
????var?s?int64
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
//?SumFloats?adds?together?the?values?of?m.
func?SumFloats(m?map[string]float64)?float64?{
????var?s?float64
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
//?SumIntsOrFloats?sums?the?values?of?map?m.?It?supports?both?floats?and?integers
//?as?map?values.
func?SumIntsOrFloats[K?comparable,?V?int64?|?float64](m?map[K]V)?V?{
????var?s?V
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}
//?SumNumbers?sums?the?values?of?map?m.?Its?supports?both?integers
//?and?floats?as?map?values.
func?SumNumbers[K?comparable,?V?Number](m?map[K]V)?V?{
????var?s?V
????for?_,?v?:=?range?m?{
????????s?+=?v
????}
????return?s
}

想要了解關(guān)于 Go 的更多資訊,還可以通過掃描的方式,進(jìn)群一起探討哦~
