你想知道的 Go 泛型都在這里
泛型現(xiàn)在進(jìn)展如何?這個(gè)友好而實(shí)用的教程將解釋泛型函數(shù)和類型是什么,為什么我們需要它們,它們?cè)?Go 中如何工作,以及我們可以在哪里使用它們。這是非常簡(jiǎn)單有趣的,讓我們開始吧!
John Arundel 是一位 Go 語言的老師兼顧問,也是《For the Love of Go》一書的作者。這是一套關(guān)于現(xiàn)代軟件工程在 Go 語言中實(shí)踐的電子書,完全面向初學(xué)者。
《For the Love of Go》是一系列有趣并且容易理解的電子書,專門介紹軟件工程在 Go 語言中的實(shí)踐。
什么是泛型
大家都知道, Go 是一種 強(qiáng)類型 語言,這意味著程序中的每個(gè)變量和值都有特定的類型,如 int 或 string 。當(dāng)我們編寫函數(shù)時(shí),我們需要在所謂的 函數(shù)簽名 中指定它們的形參類型,像這樣:
func PrintString(s string) {
這里,形參 s 的類型是 string 。我們可以想象編寫這個(gè)函數(shù)接受 int 、 float64 、任意結(jié)構(gòu)類型等形參的版本。但是當(dāng)需要處理的不僅僅是這些明確類型時(shí),多少是不太方便的,盡管我們有時(shí)可以使用 接口 來解決這個(gè)問題(例如 map[string]interface 教程 中所描述),但這種方法也有很多局限性。
Go 泛型函數(shù)
相反,現(xiàn)在我們可以聲明一個(gè) 泛型函數(shù) PrintAnything,它接受一個(gè)表示任意類型的 any 參數(shù)(我們稱它為T ),并使用它做一些事情。
這是它看起來的樣子:
func PrintAnything[T any](thing T) {
很簡(jiǎn)單對(duì)吧?這里的 any 表示T 可以是任何類型。
我們?cè)趺礃诱{(diào)用這個(gè)函數(shù)?這也同樣很簡(jiǎn)單:
PrintAnything("Hello!")
注意:我在這里描述的對(duì) Go 泛型的支持還沒有發(fā)布,但它 正在實(shí)現(xiàn)中 ,很快就會(huì)發(fā)布。現(xiàn)在你可以在 支持泛型的 Go Playground 中使用它,或者在你的項(xiàng)目中使用實(shí)驗(yàn)性的 go2go 工具 來嘗試獲得 Go 泛型支持。
約束
要實(shí)現(xiàn) PrintAnything 函數(shù)其實(shí)非常容易,因?yàn)?fmt 庫(kù)就可以打印任何東西。假設(shè)我們想實(shí)現(xiàn)我們自己版本的 strings.Join 函數(shù),它接受一個(gè) T 類型的切片,并返回一個(gè)將它們連接在一起的字符串。讓我們來試一試:
// 我有一種不好的預(yù)感 func Join[T any](things []T) (result string) { for _, v := range things { result += v.String() } return result }我們已經(jīng)創(chuàng)建了一個(gè)泛型函數(shù) Join() ,它接受一個(gè)任意類型 T 的切片參數(shù)。很好,但是現(xiàn)在我們遇到了一個(gè)問題:
output := Join([]string{"a", "b", "c"})
// v.String 沒有被定義(綁定的類型 T 沒有 String 方法)
也就是說在 Join() 函數(shù)中,我們想對(duì)每個(gè)切片元素 v 調(diào)用 .String()方法 ,將其轉(zhuǎn)換為 string 。但是 Go 需要能夠提前檢查 T 類型是否有 String()方法,然而它并不知道 T 是什么,所以它不能直接調(diào)用!
我們需要做的是稍微地約束下 T 類型。實(shí)際上我們只對(duì)具有 String() 方法的類型感興趣,而不是直接接受任何類型的 T 。任何具有這種方法的類型才能作為 Join() 函數(shù)的輸入,那么我們?nèi)绾斡?Go 表達(dá)這個(gè)約束呢?我們可以使用一個(gè) 接口 :
type Stringer interface {
String() string
}
當(dāng)給定類型實(shí)現(xiàn)了 String() 方法,現(xiàn)在我們就可以把這個(gè)約束應(yīng)用到泛型函數(shù)的類型上:
func Join[T Stringer] ...
因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Stringer保證了任何類型T的值都有 String() 方法,Go 現(xiàn)在很樂意讓我們?cè)诤瘮?shù)內(nèi)部調(diào)用它。但是,如果你嘗試使用某個(gè)未實(shí)現(xiàn) Stringer 類型的切片(例如 int )來調(diào)用 Join() 方法時(shí) ,Go 將會(huì)抱怨:
result := Join([]int{1, 2, 3})
// int 未實(shí)現(xiàn) Stringer 接口(未找到 String 方法)
可比較的約束
基于方法集的約束(如 Stringer)是有用的,但如果我們想對(duì)我們的泛型輸入做一些不涉及方法調(diào)用的事情呢?
例如,假設(shè)我們想編寫一個(gè) Equal 函數(shù),它接受兩個(gè) T類型的形參,如果它們相等則返回 true ,否則返回 false 。讓我們?cè)囈辉嚕?/p>
// 這將不會(huì)有效
func Equal[T any](a, b T) bool {
return a == b
}
fmt.Println(Equal(1, 1))
// 不能比較 a == b (類型 T 沒有定義操作符 == )
這與在 Join() 中使用 String() 方法遇到的問題相同,但由于我們現(xiàn)在沒有直接調(diào)用方法,所以不能使用基于方法集的約束。相反,我們需要將T 約束為可使用 == 或 != 操作符,這被稱為 可比較 類型。幸運(yùn)的是,有一種直接的方式來指定這種類型:使用內(nèi)置的 comparable 約束,而不是 any 。
func Equal[T comparable] ...
constraints 包
增加點(diǎn)難度,假設(shè)我們想用 T的值做一些事情,既不比較它們也不調(diào)用它們的方法。例如,假設(shè)我們想為泛型 T 類型編寫一個(gè) Max() 函數(shù),它接受 T 的一個(gè)切片,并返回切片元素中的最大值。我們可以嘗試這樣做:
// Nope.
func Max[T any](input []T) (max T) {
for _, v := range input {
if v > max {
max = v
}
}
return max
}
我對(duì)此不太樂觀,但讓我們看看會(huì)發(fā)生什么:
fmt.Println(Max([]int{1, 2, 3}))
// 不能比較 v > max ( T 類型沒有定義操作符 > )
同樣,Go 不能提前驗(yàn)證 T類型可以使用 > 操作符(也就是說,T 是 有序的 )。我們?nèi)绾谓鉀Q這個(gè)問題?我們可以簡(jiǎn)單地在約束中列出所有可能允許的類型,像這樣(稱為 列表類型 ):
type Ordered interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
幸運(yùn)的是,在標(biāo)準(zhǔn)庫(kù)的 constraints 包中已經(jīng)為我們定義了一些實(shí)用的約束條件,所以我們只需要?jiǎng)觿?dòng)鍵盤就可以導(dǎo)入并像這樣來使用:
func Max[T constraints.Ordered] ...問題解決了!
泛型類型
到目前為止,一切都很酷。我們知道如何編寫可以接受任何類型參數(shù)的函數(shù)。但是如果我們想要?jiǎng)?chuàng)建一個(gè)可以包含任何類型的類型呢?例如,一個(gè) “任意類型的切片” 。這其實(shí)也很簡(jiǎn)單:
type Bunch[T any] []T
這里指對(duì)于任何給定的T類型 , Bunch[T]是T類型的切片。例如, Bunch[int] 是 int 的切片。我們可以用常規(guī)的方法來創(chuàng)建該類型的值:
x := Bunch[int]{1, 2, 3}
正如你所期望的,我們可以編寫接受泛型類型的泛型函數(shù):
func PrintBunch[T any](b Bunch[T]) {
方法也同樣可以:
func (b Bunch[T]) Print() {
我們也可以對(duì)泛型類型施加約束:
type StringableBunch[T Stringer] []T
視頻:Code Club: Generics
泛型 Golang playground Go 團(tuán)隊(duì)提供了一個(gè)支持泛型的 Go Playground 版本,你可以在上面使用當(dāng)前泛型提案的實(shí)現(xiàn)(例如嘗試本教程中的代碼示例)。
泛型 Golang Playground
它的工作方式與我們所了解和喜愛的普通 Go Playground 完全相同,只是它支持本文描述的泛型語法。由于在 Playground 中不可能運(yùn)行所有的 Go 代碼(例如網(wǎng)絡(luò)調(diào)用或者訪問文件系統(tǒng)的代碼),你可以嘗試使用 go2go 工具,它可以將使用泛型的代碼翻譯成當(dāng)前 Go 版本能編譯的代碼。
Q&A
Go 泛型提案是什么
你可以在這里閱讀完整的設(shè)計(jì)文檔草稿:
類型參數(shù) - 設(shè)計(jì)草稿
Golang 會(huì)支持泛型嗎
是的。正如本教程的概述,在 Go 中目前對(duì)于支持泛型的提案已經(jīng)在 2020 年 6 月一篇博客文章:泛型的下一階段 中宣布了。并且這篇 Github issue (關(guān)于新增上文所描述形式的泛型)也已經(jīng)被接受了。
Go 博客 表示,在 Go 1.18 的測(cè)試版本可能會(huì)包含對(duì)泛型的支持,該測(cè)試版本將于 2021 年 12 月發(fā)布。
在此之前,你可以使用 泛型 Playground 來試驗(yàn)它,并嘗試運(yùn)行此文的示例。
泛型 vs 接口:這是泛型的另一種選擇嗎
正如我在 map[string]interface 教程 中提到的,我們可以通過 接口 來編寫 Go 代碼處理任何類型的值,而不需要使用泛型函數(shù)或類型。但是,如果你想編寫實(shí)現(xiàn)任意類型的集合之類的庫(kù),那么使用泛型類型要比使用接口簡(jiǎn)單得多,也方便得多。
any 因何而來
當(dāng)定義泛型函數(shù)或類型時(shí),輸入類型必須有一個(gè)約束。類型約束可以是接口(如 Stringer )、列表類型(如 constraints.ordered)或關(guān)鍵字 comparable。但如果你真的不想要約束,也就是說,像字面意義上的 任何 T 類型 ?
符合邏輯的方法是使用 interface{} (接口對(duì)類型的方法集沒有任何限制)來表達(dá)。由于這是一個(gè)常見的約束,所以預(yù)先聲明關(guān)鍵字 any 被提供來作為 interface{} 的別名。但是你只能在類型約束中使用這個(gè)關(guān)鍵字,所以 any 并不是等價(jià)于 interface{} 。
我可以使用代碼生成器代替泛型嗎
在 Go 的泛型出現(xiàn)之前,“代碼生成器” 方法是處理此類問題的另一種傳統(tǒng)方法。本質(zhì)上,針對(duì)每種你的庫(kù)中需要處理的特定類型,它都需要使用 go 生成器工具 產(chǎn)生新的 Go 代碼。
這雖然可行,但使用起來很笨拙,它的靈活性受到限制,并且需要額外的構(gòu)建步驟。雖然代碼生成器在某些情況下仍然有用,但我們不再需要使用它來模擬 Go 中的泛型函數(shù)和類型。
什么是合約
早期的 設(shè)計(jì)草案 中泛型使用了與我們今天相似的語法,但是它使用了一個(gè)新的關(guān)鍵字 contract 來實(shí)現(xiàn)類型約束,而非現(xiàn)有的 interface 。由于種種原因,它不太受歡迎,現(xiàn)在已經(jīng)被廢棄了。
Further reading 延伸閱讀
一個(gè)增加泛型的提案(https://go.dev/blog/generics-proposal) 泛型的下一階段(https://go.dev/blog/generics-next-step) 為什么使用泛型?(https://go.dev/blog/why-generics) Go 泛型:將設(shè)計(jì)草案應(yīng)用到真實(shí)的用例中(https://secrethub.io/blog/go-generics/) 在 Go 中嘗試泛型(https://medium.com/swlh/experimenting-with-generics-in-go-39ffa155d6a1)
原文地址:https://bitfieldconsulting.com/golang/generics
原文作者:John Arundel
本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w13_Generics_in_Go.md
譯者:haoheipi
校對(duì):
想要了解更多資訊,還可以入群和大家一起暢聊哦~

