跟著 Go 作者學(xué)泛型
閱讀本文大概需要 5 分鐘。
大家好,我是 polarisxu。
在 GopherCon 2021 年大會(huì)上,Go 兩位作者 Robert Griesemer 和 Ian Lance Taylor 做了泛型相關(guān)的演講,即將在 Go1.18 發(fā)布的 Go 泛型,正是兩位設(shè)計(jì)的。一直想著把他們的演講做一個(gè)梳理,然后分享給大家。拖的有點(diǎn)久,趁春節(jié)假期整理出來(lái)了。
注意,本文中的 constraints 包,已經(jīng)確定在 Go1.18 正式版中去除,放入 golang.org/x/exp 中。詳細(xì)可以參考該文:Go1.18 這個(gè)包確定沒(méi)了。
Go1.18 關(guān)于泛型部分,主要有三個(gè)特性:
Type parameters for functions and types,即函數(shù)和類型的類型參數(shù) Type sets defined by interfaces,即由接口定義的類型集合 Type inference,即類型推斷
1、類型參數(shù)
先看函數(shù)的類型參數(shù)。
類型參數(shù)列表(Type parameter lists)
類型參數(shù)列表看起來(lái)是帶方括號(hào)的普通參數(shù)列表。通常,類型參數(shù)以大寫字母開頭,以強(qiáng)調(diào)它們是類型:
[P,?Q?constraint1,?R?constraint2]
看一個(gè)例子。
非泛型版本的求最小值:
func?min(x,?y?float64)?float64?{
??if?x?????return?x
??}
??return?y
}
如果有 int 類型的 min 版本需求,得另外寫一個(gè)類似的函數(shù),這完全是重復(fù)代碼。
那泛型版本呢?
func?min[T?constraints.Ordered](x,?y?T)?T?{
??if?x?????return?x
??}
??return?y
}
注意和上面版本的區(qū)別。
多了一個(gè) [T constraints.Ordered],這就是類型參數(shù)列表,聲明了一個(gè)類型 T,它的約束是 constraints.Ordered,即類型 T 滿足它規(guī)定的條件參數(shù)類型 float64 變成了 T,而不是具體的某個(gè)類型
那這個(gè)泛型函數(shù)如何調(diào)用呢?
m?:=?min[int](2,?3)
是不是很奇怪?其實(shí)仔細(xì)一琢磨,好像沒(méi)問(wèn)題。因?yàn)楹瘮?shù)聲明中有 ?[T constraints.Ordered],跟普通的函數(shù)參數(shù)有點(diǎn)像。調(diào)用時(shí),提供 int,表明普通函數(shù)參數(shù)是 int 類型。
實(shí)例化
在調(diào)用時(shí),會(huì)進(jìn)行實(shí)例化過(guò)程:
1)用類型實(shí)參(type arguments)替換類型形參(type parameters)
2)檢查類型實(shí)參(type arguments)是否實(shí)現(xiàn)了類型約束
如果第 2 步失敗,實(shí)例化(調(diào)用)失敗。
所以,調(diào)用過(guò)程可以分解為以下兩步:
fmin?:=?min[float64]
m?:=?fmin(2.3,?3.4)
//?和下面等價(jià)
m?:=?min[float64](2.3,?3.4)
//?相當(dāng)于?m?:=?(min[float64])(2.3,?3.4)
所以,實(shí)例化產(chǎn)生了一個(gè)非泛型函數(shù)。
類型的類型參數(shù)
類型也可以有類型參數(shù)。通過(guò)一個(gè)例子理解一下。
一個(gè)泛型版二叉樹:
type?Tree[T?interface{}]?struct?{
?left,?right?*Tree[T]
?data????????T
}
func?(t?*Tree[T])?Lookup(x?T)?*Tree[T]
var?stringTree?Tree[string]
注意其中的 [T interface{}] ,跟函數(shù)的類型參數(shù)語(yǔ)法是一樣的,T 相當(dāng)于是一個(gè)類型,所以,之后用到 Tree 的地方,T 都跟隨著,即 Tree[T],包括方法的接收者(receiver)。
注意實(shí)例化的地方:var stringTree Tree[string],和上面兩個(gè)實(shí)例化步驟中的第一步一樣。
2、類型集合(Type sets)
先看值參數(shù)的類型(the type of value parameters)。
函數(shù)普通參數(shù)列表中的每個(gè)值參數(shù)都有一個(gè)類型,這個(gè)類型定義值的集合。比如 float64 定義了浮點(diǎn)數(shù)值的集合。
相應(yīng)的有類型參數(shù)的類型(the type of type parameters),也就是說(shuō),類型參數(shù)列表中的每個(gè)類型參數(shù)都有一個(gè)類型,這個(gè)類型定義了類型的集合,這叫做類型約束(type constraint):
func?min[T?constraints.Ordered](x,?y?T)?T
這里的 constraints.Ordered 是類型參數(shù)列表中的 T 參數(shù)的類型,它定義了類型的集合,即類型約束。
constraints.Ordered 是 Go1.18 內(nèi)置的一個(gè)類型約束,它有兩個(gè)功能:
只有值支持排序的類型才能傳遞給類型參數(shù) T; T 類型的值必須支持 <操作符,因?yàn)楹瘮?shù)體中使用了該操作符。
類型約束是接口
大家都知道接口定義了方法集(method sets),演講中給了一張圖:

根據(jù) Go 的規(guī)則,類型 P、Q、R 方法中包含了 a、b、c,因此它們實(shí)現(xiàn)了接口。
所以,反過(guò)來(lái)可以說(shuō),接口也定義了類型集(type sets):

上圖中,類型 P、Q、R 都實(shí)現(xiàn)了左邊的接口(因?yàn)槎紝?shí)現(xiàn)了接口的方法集),因此我們可以說(shuō)該接口定義了類型集。
既然接口是定義類型集,只不過(guò)是間接定義的:類型實(shí)現(xiàn)接口的方法集。而類型約束是類型集,因此完全可以重用接口的語(yǔ)義,只不過(guò)這次是直接定義類型集:

這就是類型約束的語(yǔ)法,通過(guò)接口直接定義類型集:
type?Xxx?interface?{
??int?|?string?|?bool
}
而 constraints.Ordered 的定義如下:
//?Ordered?is?a?constraint?that?permits?any?ordered?type:?any?type
//?that?supports?the?operators?<=?>=?>.
//?If?future?releases?of?Go?add?new?ordered?types,
//?this?constraint?will?be?modified?to?include?them.
type?Ordered?interface?{
?Integer?|?Float?|?~string
}
Ordered 定義了所有 interger、浮點(diǎn)數(shù)和字符串類型的集合。所以,< 操作符也是支持的。這其中的 Integer、Float 也在 constraints 包有定義。
細(xì)心的朋友應(yīng)該發(fā)現(xiàn)了 ~string,類型前面的 ~。~T 意味著包含底層類型 T 的所有類型集合。
如果約束中的所有類型都支持一個(gè)操作,則該操作可以與相應(yīng)的類型參數(shù)一起使用
除了將約束單獨(dú)定義為類型外,還可以寫成字面值的形式,比如:
[S?interface{~[]E},?E?interface{}]
這看著有點(diǎn)暈,其實(shí)可以直接這么寫:
[S?~[]E,?E?interface{}]
Go1.18 中,any 是 interface{} 的別名,因此可以進(jìn)一步寫為:
[S?~[]E,?E?any]
E 是切片的元素類型,~[]E 表示底層是 []E 切片類型的都符合該約束。
3、類型推斷
在調(diào)用泛型函數(shù)時(shí),提供類型實(shí)參感覺有點(diǎn)多余。Go 雖然是靜態(tài)類型語(yǔ)言,但擅長(zhǎng)類型推斷。因此泛型這里,Go 也實(shí)現(xiàn)了類型推斷。
調(diào)用泛型版的 min,可以不提供類型實(shí)參,而是直接由 Go 進(jìn)行類型推斷:
var?a,?b,?m?float64
m?:=?min[float64](a,?b)
類型推斷的細(xì)節(jié)很復(fù)雜,但使用起來(lái)還是很簡(jiǎn)單,大部分時(shí)候,跟普通函數(shù)調(diào)用沒(méi)有區(qū)別。
關(guān)于類型推斷,演講中給了一個(gè)例子:
func?Scale[E?constraints.Integer](s?[]E,?c?E)?[]E?{
?r?:=?make([]E,?len(s))
?for?i,?v?:=?range?s?{
??r[i]?=?v?*?c
?}
?return?r
}
這個(gè)函數(shù)的目的是希望對(duì) s 中的每個(gè)元素都乘以參數(shù) c,最后返回一個(gè)新的切片。
接著定義一個(gè)類型:
type?Point?[]int32
func?(p?Point)?String()?string?{
?//?實(shí)現(xiàn)細(xì)節(jié)不重要,忽略
?return?"point"
}
很顯然,Point 類型的切片可以傳遞給 Scale:
func?ScaleAndPrint(p?Point)?{
?r?:=?Scale(p,?2)
?fmt.Println(r.String())
}
我們希望對(duì) p 進(jìn)行 Scale,得到一個(gè)新的 p,但發(fā)現(xiàn)返回的 r 根本不是 Point:
func?main()?{
?p?:=?Point{3,?2,?4}
?ScaleAndPrint(p)
}
會(huì)報(bào)錯(cuò):r.String undefined (type []int32 has no field or method String)。
所以,我們應(yīng)該這樣修改 Scale 函數(shù):
func?Scale[S?~[]E,?E?constraints.Integer](s?S,?c?E)?S?{
?r?:=?make(S,?len(s))
?for?i,?v?:=?range?s?{
??r[i]?=?v?*?c
?}
?return?r
}
注意其中的變化:加入了泛型 S,以及額外的類型約束 ~[]E。
調(diào)用 Scale 時(shí),不需要 r := Scale[Point, int32](p, 2),因?yàn)?Go 會(huì)進(jìn)行類型推斷。
正確的完整代碼如下:
package?main
import?(
?"constraints"
?"fmt"
)
func?Scale[S?~[]E,?E?constraints.Integer](s?S,?c?E)?S?{
?r?:=?make(S,?len(s))
?for?i,?v?:=?range?s?{
??r[i]?=?v?*?c
?}
?return?r
}
type?Point?[]int32
func?(p?Point)?String()?string?{
?//?實(shí)現(xiàn)細(xì)節(jié)不重要,忽略
?return?"point"
}
func?ScaleAndPrint(p?Point)?{
?r?:=?Scale(p,?2)
?fmt.Println(r.String())
}
func?main()?{
?p?:=?Point{3,?2,?4}
?ScaleAndPrint(p)
}
4、什么時(shí)候用泛型
泛型的加入,無(wú)疑增加了復(fù)雜度。我個(gè)人認(rèn)為,能不用泛型就不用泛型。在演講中,兩位大佬提到,在以下場(chǎng)景可以考慮使用泛型:
對(duì)于 slice、map、channel 等類型,如果它們的元素類型是不確定的,操作這類類型的函數(shù)可以考慮用泛型 一些通用目的的數(shù)據(jù)結(jié)構(gòu),比如前面提到的二叉樹等 如果一些函數(shù)行為相同,只是類型不同,可以考慮用泛型重構(gòu)
注意,目前 Go 方法不支持類型參數(shù),所以,如果方法有需要泛型的場(chǎng)景,可以轉(zhuǎn)為函數(shù)的形式。
此外,不要為了泛型而泛型。比如這樣的泛型就很糟糕:
func?ReadFour[T?io.Reader](r?T)?([]byte,?error)
而應(yīng)該使用非泛型版本:
func?ReadFour(r?io.Reader)?([]byte,?error)
5、總結(jié)
泛型是一把雙刃劍。泛型的加入,讓 Go 不那么簡(jiǎn)單了。有些代碼寫出來(lái),可讀性可能非常差。我們應(yīng)該按沒(méi)有泛型時(shí)候?qū)懘a,當(dāng)發(fā)現(xiàn)在 Repeat Yourself 時(shí),再考慮能不能用泛型重構(gòu),千萬(wàn)別玩什么花樣!
最后,放上演講的視頻地址,有興趣的可以觀看:https://www.youtube.com/watch?v=Pa_e9EeCdy8。
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語(yǔ)言并創(chuàng)建了 Go 語(yǔ)言中文網(wǎng)!著有《Go語(yǔ)言編程之旅》、開源圖書《Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場(chǎng)心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長(zhǎng)!也歡迎加我微信好友交流:gopherstudio
