前言
本教程介紹 Go 中泛型的基礎(chǔ)知識面哼。使用泛型棵磷,你可以聲明和使用編寫為與調(diào)用代碼提供的任何一組類型一起使用的函數(shù)或類型展蒂。
在本教程中,你將聲明兩個簡單的非泛型函數(shù)抖单,然后在單個泛型函數(shù)中實(shí)現(xiàn)相同的邏輯岔激。
你將逐步完成以下部分:
- 為你的代碼創(chuàng)建一個文件夾勒极。
- 添加非泛型函數(shù)。
- 添加一個通用函數(shù)來處理多種類型虑鼎。
- 調(diào)用泛型函數(shù)時刪除類型參數(shù)辱匿。
- 聲明類型約束。
注意:有關(guān)其他教程炫彩,請參閱教程匾七。
注意:如果你愿意,可以使用 “Go dev 分支”模式下的 Go Playground 來編輯和運(yùn)行你的程序昨忆。
先決條件
-
Go 1.18
或更高版本的安裝。有關(guān)安裝說明奖磁,請參閱安裝 Go。 - 用于編輯代碼的工具独旷。你擁有的任何文本編輯器都可以正常工作寥裂。
- 一個命令終端嵌洼。Go 在 Linux 和 Mac 上的任何終端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。
為你的代碼創(chuàng)建一個文件夾
首先低飒,為你要編寫的代碼創(chuàng)建一個文件夾糕档。
- 打開命令提示符并切換到你的主目錄端仰。
在 Linux 或 Mac 上:
$ cd
在 Windows 上:
C:\> cd %HOMEPATH%
本教程的其余部分將顯示 $
作為提示鹤竭。你使用的命令也可以在 Windows
上運(yùn)行。
- 在命令提示符下苍碟,為你的代碼創(chuàng)建一個名為
generics
的目錄。
$ mkdir generics
$ cd generics
- 創(chuàng)建一個模塊來保存你的代碼早芭。
運(yùn)行 go mod init
命令匀谣,為其提供新代碼的模塊路徑。
$ go mod init example/generics
go: creating new go.mod: module example/generics
注意:對于生產(chǎn)代碼霹疫,你需要指定一個更符合你自己需求的模塊路徑痕寓。有關(guān)更多信息任内,請務(wù)必查看管理依賴項(xiàng)摘盆。
接下來狈邑,你將添加一些簡單的代碼來處理 maps
坦弟。
添加非泛型函數(shù)
在此步驟中,你將添加兩個函數(shù)官地,每個函數(shù)將 map
的值相加并返回總數(shù)酿傍。
你要聲明兩個函數(shù)而不是一個,因?yàn)槟阏谑褂脙煞N不同類型的映射:一種用于存儲 int64
值驱入,另一種用于存儲 float64
值赤炒。
編寫代碼
使用你的文本編輯器,在
generics
目錄中創(chuàng)建一個名為main.go
的文件亏较。你將在此文件中編寫你的Go
代碼莺褒。進(jìn)入
main.go
,在文件頂部雪情,粘貼以下包聲明遵岩。
package main
獨(dú)立程序(與庫相反)始終位于 package
中 main
。
- 在包聲明下方巡通,粘貼以下兩個函數(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ù)以將地圖的值相加并返回總和宴凉。
-
SumFloats
string
到float64
值的map
誊锭。 -
SumInts
string
到int64
值的map
。
- 在
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))
}
在此代碼中籽暇,你:
- 初始化一個
float64
map 和一個int64
map温治,每個都有兩個條目。 - 調(diào)用你之前聲明的兩個函數(shù)來查找每個 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
使用泛型,你可以在此處編寫一個函數(shù)而不是兩個劫窒。接下來本今,你將為包含整數(shù)或浮點(diǎn)值的映射添加一個通用函數(shù)。
添加通用函數(shù)來處理多種類型
在本節(jié)中,你將添加一個通用函數(shù)冠息,該函數(shù)可以接收包含整數(shù)或浮點(diǎn)值的映射挪凑,從而有效地將你剛剛編寫的兩個函數(shù)替換為一個函數(shù)。
要支持任一類型的值逛艰,該單個函數(shù)將需要一種方法來聲明它支持的類型躏碳。另一方面,調(diào)用代碼需要一種方法來指定它是使用整數(shù)映射還是浮點(diǎn)映射散怖。
為了支持這一點(diǎn)菇绵,你將編寫一個函數(shù),該函數(shù)在其普通函數(shù)參數(shù)之外還聲明類型參數(shù)镇眷。這些類型參數(shù)使函數(shù)具有通用性咬最,使其能夠處理不同類型的參數(shù)。你將使用類型參數(shù)和普通函數(shù)參數(shù)調(diào)用該函數(shù)欠动。
每個類型參數(shù)都有一個類型約束永乌,它充當(dāng)類型參數(shù)的一種元類型。每個類型約束指定調(diào)用代碼可用于相應(yīng)類型參數(shù)的允許類型參數(shù)具伍。
雖然類型參數(shù)的約束通常表示一組類型翅雏,但在編譯時,類型參數(shù)代表單一類型——調(diào)用代碼作為類型參數(shù)提供的類型沿猜。如果類型參數(shù)的約束不允許類型參數(shù)的類型枚荣,則代碼將無法編譯。
請記住啼肩,類型參數(shù)必須支持泛型代碼對其執(zhí)行的所有操作。例如衙伶,如果你的函數(shù)代碼嘗試對其 string 約束包括數(shù)字類型的類型參數(shù)執(zhí)行操作(例如索引)祈坠,則代碼將無法編譯。
在你即將編寫的代碼中矢劲,你將使用允許整數(shù)或浮點(diǎn)類型的約束赦拘。
編寫代碼
- 在你之前添加的兩個函數(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
}
在此代碼中躺同,你:
- 聲明一個
SumIntsOrFloats
具有兩個類型參數(shù)(在方括號內(nèi))K
和V
的函數(shù),以及一個使用類型參數(shù)的參數(shù)丸逸,m
類型為map[K]V
蹋艺。該函數(shù)返回一個類型的值V
。 - 為
K
類型參數(shù)指定類型約束comparable
黄刚。專門針對此類情況捎谨,comparable
在Go
中預(yù)先聲明了約束。它允許任何類型的值可以用作比較運(yùn)算符==
和的操作數(shù)!=
。Go
要求map keys
具有可比性涛救。所以聲明K as comparable
是必要的畏邢,這樣你就可以K
在map
變量中用作鍵。它還確保調(diào)用代碼對map keys
使用允許的類型检吆。 - 為
V
類型參數(shù)指定一個約束舒萎,它是兩種類型的聯(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)建的每個映射酥泛。
- 指定類型參數(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ù)底瓣。正如你將在下一節(jié)中看到的谢揪,在許多情況下你可以省略這些類型參數(shù)蕉陋,因?yàn)榫幾g器可以推斷它們。
調(diào)用泛型函數(shù)時刪除類型參數(shù)
在本節(jié)中拨扶,你將添加通用函數(shù)調(diào)用的修改版本凳鬓,進(jìn)行小的更改以簡化調(diào)用代碼。你將刪除在這種情況下不需要的類型參數(shù)患民。
當(dāng) Go
編譯器可以推斷你要使用的類型時缩举,你可以在調(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ù)队塘。
聲明類型約束
在最后一部分中袁梗,你將把之前定義的約束移到它自己的接口中,以便你可以在多個地方重用它憔古。以這種方式聲明約束有助于簡化代碼围段,例如當(dāng)約束更復(fù)雜時。
你將類型約束聲明為接口投放。約束允許任何類型實(shí)現(xiàn)接口。例如适贸,如果你聲明了具有三個方法的類型約束接口灸芳,然后在泛型函數(shù)中將其與類型參數(shù)一起使用,則用于調(diào)用該函數(shù)的類型參數(shù)必須具有所有這些方法拜姿。
正如你將在本節(jié)中看到的烙样,約束接口也可以引用特定類型。
編寫代碼
- 就在上面
main
蕊肥,緊跟在import
語句之后谒获,粘貼以下代碼來聲明類型約束蛤肌。
type Number interface {
int64 | float64
}
在此代碼中,你:
聲明
Number
要用作類型約束的接口類型批狱。在接口內(nèi)部聲明一個并集
int64
和float64
本質(zhì)上裸准,你正在將聯(lián)合從函數(shù)聲明移動到新的類型約束中。這樣赔硫,當(dāng)你想將類型參數(shù)約束為 int64
或者 float64
時炒俱,你可以使用此 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
}
在此代碼中,你:
- 聲明一個與你之前聲明的泛型函數(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
每個map
,打印每個map
的總和控嗜。
與上一節(jié)一樣茧彤,在調(diào)用泛型函數(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 中的泛型珠洗。
建議的下一個主題:
-
Go Tour是對
Go
基礎(chǔ)知識的逐步介紹。 - 你將在 Effective Go 和 How to write Go code 中找到有用的
Go
最佳實(shí)踐若专。
完整的代碼
你可以在 Go playground 上運(yùn)行這個程序许蓖。在 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
}