前言
本教程介紹 Go 中泛型的基礎(chǔ)知識。使用泛型,你可以聲明和使用編寫為與調(diào)用代碼提供的任何一組類型一起使用的函數(shù)或類型。
在本教程中,你將聲明兩個(gè)簡單的非泛型函數(shù),然后在單個(gè)泛型函數(shù)中實(shí)現(xiàn)相同的邏輯。
你將逐步完成以下部分:
- 為你的代碼創(chuàng)建一個(gè)文件夾。
- 添加非泛型函數(shù)。
- 添加一個(gè)通用函數(shù)來處理多種類型。
- 調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù)。
- 聲明類型約束。
注意:有關(guān)其他教程,請參閱教程。
注意:如果你愿意,可以使用 “Go dev 分支”模式下的 Go Playground 來編輯和運(yùn)行你的程序。
先決條件
-
Go 1.18或更高版本的安裝。有關(guān)安裝說明,請參閱安裝 Go。 - 用于編輯代碼的工具。你擁有的任何文本編輯器都可以正常工作。
- 一個(gè)命令終端。Go 在 Linux 和 Mac 上的任何終端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。
為你的代碼創(chuàng)建一個(gè)文件夾
首先,為你要編寫的代碼創(chuàng)建一個(gè)文件夾。
- 打開命令提示符并切換到你的主目錄。
在 Linux 或 Mac 上:
$ cd
在 Windows 上:
C:\> cd %HOMEPATH%
本教程的其余部分將顯示 $ 作為提示。你使用的命令也可以在 Windows 上運(yùn)行。
- 在命令提示符下,為你的代碼創(chuàng)建一個(gè)名為
generics的目錄。
$ mkdir generics
$ cd generics
- 創(chuàng)建一個(gè)模塊來保存你的代碼。
運(yùn)行 go mod init 命令,為其提供新代碼的模塊路徑。
$ go mod init example/generics
go: creating new go.mod: module example/generics
注意:對于生產(chǎn)代碼,你需要指定一個(gè)更符合你自己需求的模塊路徑。有關(guān)更多信息,請務(wù)必查看管理依賴項(xiàng)。
接下來,你將添加一些簡單的代碼來處理 maps。
添加非泛型函數(shù)
在此步驟中,你將添加兩個(gè)函數(shù),每個(gè)函數(shù)將 map 的值相加并返回總數(shù)。
你要聲明兩個(gè)函數(shù)而不是一個(gè),因?yàn)槟阏谑褂脙煞N不同類型的映射:一種用于存儲(chǔ) int64 值,另一種用于存儲(chǔ) float64 值。
編寫代碼
使用你的文本編輯器,在
generics目錄中創(chuàng)建一個(gè)名為main.go的文件。你將在此文件中編寫你的Go代碼。進(jìn)入
main.go,在文件頂部,粘貼以下包聲明。
package main
獨(dú)立程序(與庫相反)始終位于 package 中 main。
- 在包聲明下方,粘貼以下兩個(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ù)以將地圖的值相加并返回總和。
-
SumFloatsstring到float64值的map。 -
SumIntsstring到int64值的map。
- 在
main.go頂部的包聲明下方,粘貼以下main函數(shù)以初始化兩個(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è)
float64map 和一個(gè)int64map,每個(gè)都有兩個(gè)條目。 - 調(diào)用你之前聲明的兩個(gè)函數(shù)來查找每個(gè) map 值的總和。
- 打印結(jié)果。
- 在
main.go頂部附近,就在包聲明的下方,導(dǎo)入你需要支持你剛剛編寫的代碼的包。
第一行代碼應(yīng)如下所示:
package main
import "fmt"
- 保存
main.go。
運(yùn)行代碼
從包含 main.go 的目錄中的命令行,運(yùn)行代碼。
$ go run .
Non-Generic Sums: 46 and 62.97
使用泛型,你可以在此處編寫一個(gè)函數(shù)而不是兩個(gè)。接下來,你將為包含整數(shù)或浮點(diǎn)值的映射添加一個(gè)通用函數(shù)。
添加通用函數(shù)來處理多種類型
在本節(jié)中,你將添加一個(gè)通用函數(shù),該函數(shù)可以接收包含整數(shù)或浮點(diǎn)值的映射,從而有效地將你剛剛編寫的兩個(gè)函數(shù)替換為一個(gè)函數(shù)。
要支持任一類型的值,該單個(gè)函數(shù)將需要一種方法來聲明它支持的類型。另一方面,調(diào)用代碼需要一種方法來指定它是使用整數(shù)映射還是浮點(diǎn)映射。
為了支持這一點(diǎn),你將編寫一個(gè)函數(shù),該函數(shù)在其普通函數(shù)參數(shù)之外還聲明類型參數(shù)。這些類型參數(shù)使函數(shù)具有通用性,使其能夠處理不同類型的參數(shù)。你將使用類型參數(shù)和普通函數(shù)參數(shù)調(diào)用該函數(shù)。
每個(gè)類型參數(shù)都有一個(gè)類型約束,它充當(dāng)類型參數(shù)的一種元類型。每個(gè)類型約束指定調(diào)用代碼可用于相應(yīng)類型參數(shù)的允許類型參數(shù)。
雖然類型參數(shù)的約束通常表示一組類型,但在編譯時(shí),類型參數(shù)代表單一類型——調(diào)用代碼作為類型參數(shù)提供的類型。如果類型參數(shù)的約束不允許類型參數(shù)的類型,則代碼將無法編譯。
請記住,類型參數(shù)必須支持泛型代碼對其執(zhí)行的所有操作。例如,如果你的函數(shù)代碼嘗試對其 string 約束包括數(shù)字類型的類型參數(shù)執(zhí)行操作(例如索引),則代碼將無法編譯。
在你即將編寫的代碼中,你將使用允許整數(shù)或浮點(diǎn)類型的約束。
編寫代碼
- 在你之前添加的兩個(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è)
SumIntsOrFloats具有兩個(gè)類型參數(shù)(在方括號內(nèi))K和V的函數(shù),以及一個(gè)使用類型參數(shù)的參數(shù),m類型為map[K]V。該函數(shù)返回一個(gè)類型的值V。 - 為
K類型參數(shù)指定類型約束comparable。專門針對此類情況,comparable在Go中預(yù)先聲明了約束。它允許任何類型的值可以用作比較運(yùn)算符==和的操作數(shù)!=。Go要求map keys具有可比性。所以聲明K as comparable是必要的,這樣你就可以K在map變量中用作鍵。它還確保調(diào)用代碼對map keys使用允許的類型。 - 為
V類型參數(shù)指定一個(gè)約束,它是兩種類型的聯(lián)合:int64和float64。使用|指定兩種類型的聯(lián)合,這意味著此約束允許任何一種類型。編譯器將允許任一類型作為調(diào)用代碼中的參數(shù)。 - 指定
m參數(shù)是type map[K]V,其中K和V是已經(jīng)為類型參數(shù)指定的類型。請注意,我們知道map[K]V是有效的map類型,因?yàn)?K它是可比較的類型。如果我們沒有聲明K可比較,編譯器將拒絕對map[K]V的引用。
在 main.go 中,在你已有的代碼下方,粘貼以下代碼。
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
在此代碼中,你:
- 調(diào)用你剛剛聲明的通用函數(shù),傳遞你創(chuàng)建的每個(gè)映射。
- 指定類型參數(shù) - 方括號中的類型名稱 - 以明確應(yīng)該替換你正在調(diào)用的函數(shù)中的類型參數(shù)的類型。
- 正如你將在下一節(jié)中看到的,你通常可以在函數(shù)調(diào)用中省略類型參數(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ù)替換為該調(diào)用中指定的具體類型。
在調(diào)用你編寫的泛型函數(shù)時(shí),你指定了類型參數(shù),告訴編譯器使用什么類型代替函數(shù)的類型參數(shù)。正如你將在下一節(jié)中看到的,在許多情況下你可以省略這些類型參數(shù),因?yàn)榫幾g器可以推斷它們。
調(diào)用泛型函數(shù)時(shí)刪除類型參數(shù)
在本節(jié)中,你將添加通用函數(shù)調(diào)用的修改版本,進(jìn)行小的更改以簡化調(diào)用代碼。你將刪除在這種情況下不需要的類型參數(shù)。
當(dāng) Go 編譯器可以推斷你要使用的類型時(shí),你可以在調(diào)用代碼中省略類型參數(shù)。編譯器從函數(shù)參數(shù)的類型推斷類型參數(shù)。
請注意,這并不總是可能的。例如,如果你需要調(diào)用沒有參數(shù)的泛型函數(shù),則需要在函數(shù)調(diào)用中包含類型參數(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ù)的并集捕獲到你可以重用的類型約束(例如從其他代碼中)來進(jìn)一步簡化函數(shù)。
聲明類型約束
在最后一部分中,你將把之前定義的約束移到它自己的接口中,以便你可以在多個(gè)地方重用它。以這種方式聲明約束有助于簡化代碼,例如當(dāng)約束更復(fù)雜時(shí)。
你將類型約束聲明為接口。約束允許任何類型實(shí)現(xiàn)接口。例如,如果你聲明了具有三個(gè)方法的類型約束接口,然后在泛型函數(shù)中將其與類型參數(shù)一起使用,則用于調(diào)用該函數(shù)的類型參數(shù)必須具有所有這些方法。
正如你將在本節(jié)中看到的,約束接口也可以引用特定類型。
編寫代碼
- 就在上面
main,緊跟在import語句之后,粘貼以下代碼來聲明類型約束。
type Number interface {
int64 | float64
}
在此代碼中,你:
聲明
Number要用作類型約束的接口類型。在接口內(nèi)部聲明一個(gè)并集
int64和float64
本質(zhì)上,你正在將聯(lián)合從函數(shù)聲明移動(dòng)到新的類型約束中。這樣,當(dāng)你想將類型參數(shù)約束為 int64 或者 float64 時(shí),你可以使用此 Number 類型約束而不是寫出 int64 | float64。
- 在你已有的函數(shù)下方,粘貼以下通用
SumNumbers函數(shù)。
// SumNumbers sums the values of map m. It 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ù),但使用新的接口類型而不是聯(lián)合作為類型約束。和以前一樣,你使用類型參數(shù)作為參數(shù)和返回類型。
- 在
main.go中,在你已有的代碼下方,粘貼以下代碼。
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
在此代碼中,你:
- 調(diào)用
SumNumbers每個(gè)map,打印每個(gè)map的總和。
與上一節(jié)一樣,在調(diào)用泛型函數(shù)時(shí)省略了類型參數(shù)(方括號中的類型名稱)。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é)論
做得很好!你剛剛向自己介紹了 Go 中的泛型。
建議的下一個(gè)主題:
-
Go Tour是對
Go基礎(chǔ)知識的逐步介紹。 - 你將在 Effective Go 和 How to write Go code 中找到有用的
Go最佳實(shí)踐。
完整的代碼
你可以在 Go playground 上運(yùn)行這個(gè)程序。在 playground 上,只需單擊“運(yùn)行”按鈕。
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
}