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