『每周譯Go』Go官方出品:如何使用泛型
目錄
1. 前提條件
2. 為你的代碼創(chuàng)建一個(gè)文件夾
3. 添加非泛型函數(shù)
4. 添加一個(gè)泛型函數(shù)來(lái)處理多種類型
5. 在調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù)
6. 聲明一個(gè)類型約束
7. 結(jié)論
8. 完整代碼
備注:這是一個(gè) beta 版本的內(nèi)容
這個(gè)教程介紹了 Go 泛型的基礎(chǔ)概念。通過(guò)泛型,你可以聲明并使用函數(shù)或者是類型,那些用于調(diào)用代碼時(shí)參數(shù)需要兼容多個(gè)不同類型的情況。
在這個(gè)教程里,你會(huì)聲明兩個(gè)普通的函數(shù),然后復(fù)制一份相同的邏輯到一個(gè)泛型的方法里。
你會(huì)通過(guò)以下幾個(gè)章節(jié)來(lái)進(jìn)行學(xué)習(xí):
為你的代碼創(chuàng)建一個(gè)文件夾; 添加非泛型函數(shù); 添加一個(gè)泛型函數(shù)來(lái)處理多種類型; 在調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù); 聲明一個(gè)類型約束。
備注:對(duì)其他教程,可以查看教程 備注:你同時(shí)也可以使用 "Go dev branch"模式來(lái)編輯和運(yùn)行你的代碼,如果你更喜歡以這種形式的話
前提條件
安裝 Go 1.18 Beta 1 或者更新的版本。對(duì)安裝流程,請(qǐng)看安裝并使用 beta 版本。 代碼編輯器。任何你順手的代碼編輯器。 一個(gè)命令終端。Go 在任何終端工具上都很好使用,比如 Linux 、Mac、PowerShell 或者 Windows 上的 cmd。
安裝并使用 beta 版本
這個(gè)教程需要 Beta 1 的泛型特性。安裝 beta 版本,需要通過(guò)下面幾個(gè)步驟:
1、 執(zhí)行下面的指令安裝 beta 版本
$?go?install?golang.org/dl/go1.18beta1@latest
2、 執(zhí)行下面的指令下載更新
$?go1.18beta1?download
3、用 beta 版本執(zhí)行 go 命令,而不是 Go 的發(fā)布版本 (如果你本地有安裝的話)
你可以使用 beta 版本名稱或者把 beta 重命名成別的名稱來(lái)執(zhí)行命令。
使用 beta 版本名稱,你可以通過(guò) go1.18beta1 來(lái)執(zhí)行指令而不是 go:
$?go1.18beta1?version
通過(guò)對(duì) beta 版本名稱重命名,你可以簡(jiǎn)化指令:
$?alias?go=go1.18beta1
$?go?version
在這個(gè)教程中將假設(shè)你已經(jīng)對(duì) beta 版本名稱進(jìn)行了重命名。
為你的代碼創(chuàng)建一個(gè)文件夾
在一開(kāi)始,先給你要寫(xiě)的代碼創(chuàng)建一個(gè)文件夾
1、 打開(kāi)一個(gè)命令提示符并切換到/home 文件夾
在 Linux 或者 Mac 上:
$?cd
在 windows 上:
C:\>?cd?%HOMEPATH%
在接下去的教程里面會(huì)用$來(lái)代表提示符。指令在 windows 上也適用。
2、 在命令提示符下,為你的代碼創(chuàng)建一個(gè)名為 generics 的目錄
$?mkdir?generics
$?cd?generics
3、 創(chuàng)建一個(gè) module 來(lái)存放你的代碼
執(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)看依賴管理
接下來(lái),你會(huì)增加一些簡(jiǎn)單的和 maps 相關(guān)的代碼。
添加普通函數(shù)
在這一步中,你將添加兩個(gè)函數(shù),每個(gè)函數(shù)都會(huì)累加 map 中的值 ,并返回總和。
你將聲明兩個(gè)函數(shù)而不是一個(gè),因?yàn)槟阋幚韮煞N不同類型的 map:一個(gè)存儲(chǔ) int64 類型的值,另一個(gè)存儲(chǔ) float64 類型的值。
寫(xiě)代碼
1、 用你的文本編輯器,在 generics 文件夾里面創(chuàng)建一個(gè)叫 main.go 的文件。你將會(huì)在這個(gè)文件內(nèi)寫(xiě)你的 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 類型。
4、 在 main.go 的頂部,包聲明的下面,粘貼以下 main 函數(shù),用來(lái)初始化兩個(gè) map,并在調(diào)用你在上一步中聲明的函數(shù)時(shí)將它們作為參數(shù)。
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è)方法來(lái)獲取每個(gè) map 的值的總和; 打印結(jié)果。
5、 靠近 main.go 頂部,僅在包聲明的下方,導(dǎo)入你剛剛寫(xiě)的代碼所需要引用的包。
第一行代碼應(yīng)該看起來(lái)如下所示:
package?main
import?"fmt"
6、 保存 main.go.
運(yùn)行代碼
在 main.go 所在目錄下,通過(guò)命令行運(yùn)行代碼
$?go?run?.
Non-Generic?Sums:?46?and?62.97
有了泛型,你可以只寫(xiě)一個(gè)函數(shù)而不是兩個(gè)。接下來(lái),你將為 maps 添加一個(gè)泛型函數(shù),來(lái)允許接收整數(shù)類型或者是浮點(diǎn)數(shù)類型。
添加泛型函數(shù)處理多種類型
在這一節(jié),你將會(huì)添加一個(gè)泛型函數(shù)來(lái)接收一個(gè) map,可能值是整數(shù)類型或者浮點(diǎn)數(shù)類型的 map,有效地用一個(gè)函數(shù)替換掉你剛才寫(xiě)的 2 個(gè)函數(shù)。
為了支持不同類型的值,這個(gè)函數(shù)需要有一個(gè)方法來(lái)聲明它所支持的類型。另一方面,調(diào)用代碼將需要一種方法來(lái)指定它是用整數(shù)還是浮點(diǎn)數(shù)來(lái)調(diào)用。
為了實(shí)現(xiàn)上面的描述,你將會(huì)聲明一個(gè)除了有普通函數(shù)參數(shù),還有類型參數(shù)的函數(shù)。這個(gè)類型參數(shù)實(shí)現(xiàn)了函數(shù)的通用性,使得它可以處理多個(gè)不同的類型。你將會(huì)用類型參數(shù)和普通函數(shù)參數(shù)來(lái)調(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ù)的約束所允許,代碼則無(wú)法編譯。
需要記住的是類型參數(shù)必須滿足泛型代碼對(duì)它的所有的操作。舉個(gè)例子,如果你的代碼嘗試去做一些 string 的操作 (比如索引),而這個(gè)類型參數(shù)包含數(shù)字的類型,那代碼是無(wú)法編譯的。
在下面你要編寫(xiě)的代碼里,你會(huì)使用允許整數(shù)或者浮點(diǎn)數(shù)類型的限制。
寫(xiě)代碼
1、 在你之前寫(xiě)的兩個(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)?K 是可比較的類型,所以 map[K] V 是一個(gè)有效的 map 類型。如果我們沒(méi)有聲明 K 是可比較的,那么編譯器會(huì)拒絕對(duì) map[K] V 的引用。
2、 在 main.go 里,在你現(xiàn)在的代碼下方,粘貼如下代碼:
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)的類型名稱 - 來(lái)明確你所調(diào)用的函數(shù)中應(yīng)該用哪些類型來(lái)替代類型參數(shù)。
你將會(huì)在下一節(jié)看到,你通常可以在函數(shù)調(diào)用時(shí)省略類型參數(shù)。Go 通??梢詮拇a里推斷出來(lái)。
打印函數(shù)返回的總和。
運(yùn)行代碼
在 main.go 所在目錄下,通過(guò)命令行運(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)用你寫(xiě)的泛型函數(shù)時(shí),你指定了類型參數(shù)來(lái)告訴編譯器用什么類型來(lái)替換函數(shù)的類型參數(shù)。正如你將在下一節(jié)所看到的,在許多情況下,你可以省略這些類型參數(shù),因?yàn)榫幾g器可以推斷出它們。
當(dāng)調(diào)用泛型函數(shù)時(shí)移除類型參數(shù)
在這一節(jié),你會(huì)添加一個(gè)泛型函數(shù)調(diào)用的修改版本,通過(guò)一個(gè)小的改變來(lái)簡(jiǎn)化代碼。在這個(gè)例子里你將移除掉不需要的類型參數(shù)。
當(dāng) Go 編譯器可以推斷出你要使用的類型時(shí),你可以在調(diào)用代碼中省略類型參數(shù)。編譯器從函數(shù)參數(shù)的類型中推斷出類型參數(shù)。
注意這不是每次都可行的。舉個(gè)例子,如果你需要調(diào)用一個(gè)沒(méi)有參數(shù)的泛型函數(shù),那么你需要在調(diào)用函數(shù)時(shí)帶上類型參數(shù)。
寫(xiě)代碼
在 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 所在目錄下,通過(guò)命令行運(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
接下來(lái),你將通過(guò)把整數(shù)和浮點(diǎn)數(shù)的合集定義到一個(gè)你可以重復(fù)使用的類型約束中,比如從其他的代碼,來(lái)進(jìn)一步簡(jiǎn)化這個(gè)函數(shù)。
聲明類型約束
在最后一節(jié)中,你將把你先前定義的約束移到它自己的 interface 中,這樣你就可以在多個(gè)地方重復(fù)使用它。以這種方式聲明約束有助于簡(jiǎn)化代碼,尤其當(dāng)一個(gè)約束越來(lái)越復(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 下方,粘貼如下代碼來(lái)定義類型約束。
type?Number?interface?{
????int64?|?float64
}
在這段代碼里,你:
聲明一個(gè) Number interface 類型作為類型限制
在 interface 內(nèi)聲明 int64 和 float64 的合集 本質(zhì)上,你是在把函數(shù)聲明中的合集移到一個(gè)新的類型約束中。這樣子,當(dāng)你想要約束一個(gè)類型參數(shù)為 int64 或者 float64,你可以使用 Number interface 而不是寫(xiě) int64 | float64。
2、 在你已寫(xiě)好的函數(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ù)和返回類型。3、 在 main.go,在你已寫(xiě)完的代碼下方,粘貼如下代碼。
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 所在目錄下,通過(guò)命令行運(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é)
完美結(jié)束!你剛才已經(jīng)給你自己介紹了 Go 的泛型。
如果你想繼續(xù)試驗(yàn),你可以嘗試用整數(shù)約束和浮點(diǎn)數(shù)約束來(lái)寫(xiě) Number interface,來(lái)允許更多的數(shù)字類型。
建議閱讀的相關(guān)文章:
Go Tour 是一個(gè)很好的,手把手教 Go 基礎(chǔ)的介紹。 你可以在 Effective Go 和 How to write Go 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
}
原文信息
原文地址:https://go.dev/doc/tutorial/generics
原文作者:go.dev
本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w49_Tutorial_Getting_started_with_generics.md
譯者:zxmfke
校對(duì):cvley
想要了解關(guān)于 Go 的更多資訊,還可以通過(guò)掃描的方式,進(jìn)群一起探討哦~
