Go 泛型之瞭解類型參數 目錄Go 泛型之瞭解類型參數一、Go 的泛型與其他主流編程語言的泛型差異二、返回切片中值最大的元素三、類型參數(type parameters)四、泛型函數3.1 泛型函數的結構3.2 調用泛型函數3.3 泛型函數實例化(instantiation)五、泛型類型5.1 聲明 ...
Go 泛型之瞭解類型參數
目錄一、Go 的泛型與其他主流編程語言的泛型差異
Go泛型和其他支持泛型的主流編程語言之間的泛型設計與實現存在差異一樣,Go 的泛型與其他主流編程語言的泛型也是不同的。我們先看一下 Go 泛型設計方案已經明確不支持的若幹特性,比如:
- 不支持泛型特化(specialization),即不支持編寫一個泛型函數針對某個具體類型的特殊版本;
- 不支持元編程(metaprogramming),即不支持編寫在編譯時執行的代碼來生成在運行時執行的代碼;
- 不支持操作符方法(operator method),即只能用普通的方法(method)操作類型實例(比如:getIndex(k)),而不能將操作符視為方法並自定義其實現,比如一個容器類型的下標訪問 c[k];
- 不支持變長的類型參數(type parameters);
- ......
這些特性如今不支持,後續大概率也不會支持。在進入 Go 泛型語法學習之前,一定要先瞭解 Go 團隊的這些設計決策。
二、返回切片中值最大的元素
我們先來看一個例子,實現一個函數,該函數接受一個切片作為輸入參數,然後返回該切片中值最大的那個元素。題目並沒有明確使用什麼元素類型的切片,我們就先以最常見的整型切片為例,實現一個 maxInt
函數:
// max_int.go
func maxInt(sl []int) int {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(maxInt([]int{1, 2, -4, -6, 7, 0})) // 輸出:7
}
maxInt
的邏輯十分簡單。我們使用第一個元素值 (max := sl[0]
) 作為 max
變數初值,然後與切片後面的元素 (sl[1:]
) 進行逐一比較,如果後面的元素大於 max
,則將其值賦給 max
,這樣到切片遍歷結束,我們就得到了這個切片中值最大的那個元素(即變數 max
)。
我們現在給它加一個新需求:能否針對元素為 string
類型的切片返回其最大(按字典序)的元素值呢?
答案肯定是能!我們來實現這個 maxString
函數:
// max_string.go
func maxString(sl []string) string {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(maxString([]string{"11", "22", "44", "66", "77", "10"})) // 輸出:77
}
maxString
實現了返回 string
切片中值最大元素的需求。不過從實現上來看,maxString
與 maxInt
異曲同工,只是切片元素類型不同罷了。這時如果讓你參考上述 maxInt
或 maxString
實現一個返回浮點類型切片中最大值的函數 maxFloat
,你肯定“秒秒鐘”就可以給出一個正確的實現:
// max_float.go
func maxFloat(sl []float64) float64 {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(maxFloat([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 輸出:7.07
}
問題來了!你肯定在上面三個函數發現了的“糟糕味道”:代碼重覆。上面三個函數除了切片的元素類型不同,其他邏輯都一樣。
那麼能否實現一個“通用”的函數,可以處理上面三種元素類型的切片呢?提到“通用”,你一定想到了 Go 語言提供的 any
(interface{}
的別名),我們來試試:
// max_any.go
func maxAny(sl []any) any {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
switch v.(type) {
case int:
if v.(int) > max.(int) {
max = v
}
case string:
if v.(string) > max.(string) {
max = v
}
case float64:
if v.(float64) > max.(float64) {
max = v
}
}
}
return max
}
func main() {
i := maxAny([]any{1, 2, -4, -6, 7, 0})
m := i.(int)
fmt.Println(m) // 輸出:7
fmt.Println(maxAny([]any{"11", "22", "44", "66", "77", "10"})) // 輸出:77
fmt.Println(maxAny([]any{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 輸出:7.07
}
我們看到,maxAny
利用 any
、type switch
和類型斷言(type assertion
)實現了我們預期的目標。不過這個實現並不理想,它至少有如下幾個問題:
- 若要支持其他元素類型的切片,我們需對該函數進行修改;
maxAny
的返回值類型為any
(interface{}
),要得到其實際類型的值還需要通過類型斷言轉換;- 使用
any
(interface{}
)作為輸入參數的元素類型和返回值的類型,由於存在裝箱和拆箱操作,其性能與maxInt
等比起來要遜色不少,實測數據如下:
// max_test.go
func BenchmarkMaxInt(b *testing.B) {
sl := []int{1, 2, 3, 4, 7, 8, 9, 0}
for i := 0; i < b.N; i++ {
maxInt(sl)
}
}
func BenchmarkMaxAny(b *testing.B) {
sl := []any{1, 2, 3, 4, 7, 8, 9, 0}
for i := 0; i < b.N; i++ {
maxAny(sl)
}
}
測試結果如下:
$go test -v -bench . ./max_test.go max_any.go max_int.go
goos: darwin
goarch: amd64
... ...
BenchmarkMaxInt
BenchmarkMaxInt-8 398996863 2.982 ns/op
BenchmarkMaxAny
BenchmarkMaxAny-8 85883875 13.91 ns/op
PASS
ok command-line-arguments 2.710s
我們看到,基於 any
(interface{}
) 實現的 maxAny
其執行性能要比像 maxInt
這樣的函數慢上數倍。
在 Go 1.18 版本之前,Go 的確沒有比較理想的解決類似上述“通用”問題的手段,直到 Go 1.18 版本泛型落地後,我們可以用泛型語法實現 maxGenerics
函數:
// max_generics.go
type ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
func maxGenerics[T ordered](sl []T) T {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
type myString string
func main() {
var m int = maxGenerics([]int{1, 2, -4, -6, 7, 0})
fmt.Println(m) // 輸出:7
fmt.Println(maxGenerics([]string{"11", "22", "44", "66", "77", "10"})) // 輸出:77
fmt.Println(maxGenerics([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 輸出:7.07
fmt.Println(maxGenerics([]int8{1, 2, -4, -6, 7, 0})) // 輸出:7
fmt.Println(maxGenerics([]myString{"11", "22", "44", "66", "77", "10"})) // 輸出:77
}
我們看到,從功能角度看,泛型版本的 maxGenerics
實現了預期的特性,對於 ordered
介面中聲明的那些原生類型以及以這些原生類型為底層類型(underlying type)的類型(比如示例中的 myString
),maxGenerics
都可以無縫支持。並且,maxGenerics
返回的類型與傳入的切片的元素類型一致,調用者也無需通過類型斷言做轉換。
此外,通過下麵的性能基準測試我們也可以看出,與 maxAny
相比,泛型版本的 maxGenerics
性能要好很多,但與原生版函數如 maxInt
等還有差距。性能測試如下:
$go test -v -bench . ./max_test.go max_any.go max_int.go max_generics.go
goos: darwin
goarch: amd64
BenchmarkMaxInt
BenchmarkMaxInt-8 400910706 2.983 ns/op
BenchmarkMaxAny
BenchmarkMaxAny-8 85257433 14.04 ns/op
BenchmarkMaxGenerics
BenchmarkMaxGenerics-8 209468593 5.701 ns/op
PASS
ok command-line-arguments 4.492s
通過這個例子,我們也可以看到 Go 泛型十分適合實現一些操作容器類型(比如切片、map 等)的演算法,這也是 Go 官方推薦的第一種泛型應用場景,此類容器演算法的泛型實現使得容器演算法與容器內元素類型徹底解耦!
三、類型參數(type parameters)
根據官方說法,由於“泛型”(generic
)一詞在 Go 社區中被廣泛使用,所以官方也就接納了這一說法。但 Go 泛型方案的實質是對類型參數(type parameter
)的支持,包括:
- 泛型函數(
generic function
):帶有類型參數的函數; - 泛型類型(
generic type
):帶有類型參數的自定義類型; - 泛型方法(
generic method
):泛型類型的方法。
首先,以泛型函數為例來具體說明一下什麼是類型參數。
四、泛型函數
3.1 泛型函數的結構
我們回顧一下上面的示例,maxGenerics
就是一個泛型函數,我們看一下 maxGenerics
的函數原型:
func maxGenerics[T ordered](sl []T) T {
// ... ...
}
我們看到,maxGenerics
這個函數與我們之前學過的普通 Go 函數(ordinary function
)相比,至少有兩點不同:
maxGenerics
函數在函數名稱與函數參數列表之間多了一段由方括弧括起的代碼:[T ordered]
;maxGenerics
參數列表中的參數類型以及返回值列表中的返回值類型都是T
,而不是某個具體的類型。
maxGenerics
函數原型中多出的這段代碼[T ordered]
就是 Go 泛型的類型參數列表(type parameters list
),示例中這個列表中僅有一個類型參數 T
,ordered
為類型參數的類型約束(type constraint
)。類型約束之於類型參數,就好比常規參數列表中的類型之於常規參數。
Go 語言規範規定:函數的類型參數列表位於函數名與函數參數列表之間,由方括弧括起的固定個數的、由逗號分隔的類型參數聲明組成,其一般形式如下:
func genericsFunc[T1 constraint1, T2, constraint2, ..., Tn constraintN](ordinary parameters list) (return values list)
函數一旦擁有類型參數,就可以用該參數作為常規參數列表和返回值列表中修飾參數和返回值的類型。我們繼續 maxGenerics
泛型函數為例分析,它擁有一個類型參數 T
,在常規參數列表中,T
被用作切片的元素類型;在返回值列表中,T
被用作返回值的類型。
按 Go 慣例,類型參數名的首字母通常採用大寫形式,並且類型參數必須是具名的,即便你在後續的函數參數列表、返回值列表和函數體中沒有使用該類型參數,也是這樣。比如下麵例子中的類型參數 T
:
func print[T any]() { // 正確
}
func print[any]() { // 編譯錯誤:all type parameters must be named
}
和常規參數列表中的參數名唯一一樣,在同一個類型參數列表中,類型參數名字也要唯一,下麵這樣的代碼將會導致 Go 編譯器報錯:
func print[T1 any, T1 comparable](sl []T) { // 編譯錯誤:T1 redeclared in this block
//...
}
常規參數列表中的參數有其特定作用域,即從參數聲明處開始到函數體結束。和常規參數類似,泛型函數中類型參數也有其作用域範圍,這個範圍從類型參數列表左側的方括弧[
開始,一直持續到函數體結束,如下圖所示:
類型參數的作用域也決定了類型參數的聲明順序並不重要,也不會影響泛型函數的行為,於是下麵的泛型函數聲明與上圖中的函數是等價的:
func foo[M map[E]T, T any, E comparable](m M)(E, T) {
//... ...
}
3.2 調用泛型函數
首先,我們對“類型參數”做一下細分。和普通函數有形式參數與實際參數一樣,類型參數也有類型形參(type parameter
)和類型實參(type argument
)之分。其中類型形參就是泛型函數聲明中的類型參數,以前面示例中的 maxGenerics
泛型函數為例,如下麵代碼,maxGenerics
的類型形參就是 T
,而類型實參則是在調用 maxGenerics
時實際傳遞的類型 int
:
// 泛型函數聲明:T為類型形參
func maxGenerics[T ordered](sl []T) T
// 調用泛型函數:int為類型實參
m := maxGenerics[int]([]int{1, 2, -4, -6, 7, 0})
從上面這段代碼我們也可以看出調用泛型函數與調用普通函數的區別。在調用泛型函數時,除了要傳遞普通參數列表對應的實參之外,還要顯式傳遞類型實參,比如這裡的 int。並且,顯式傳遞的類型實參要放在函數名和普通參數列表前的方括弧中。
在反覆揣摩上面代碼和說明後,你可能會提出這樣的一個問題:如果泛型函數的類型形參較多,那麼逐一顯式傳入類型實參會讓泛型函數的調用顯得十分冗長,比如:
foo[int, string, uint32, float64](1, "hello", 17, 3.14)
這樣的寫法對開發者而言顯然談不上十分友好。其實不光大家想到了這個問題,Go 團隊的泛型實現者們也考慮了這個問題,並給出瞭解決方法:函數類型實參的自動推斷(function argument type inference
)。
顧名思義,這個機制就是通過判斷傳遞的函數實參的類型來推斷出類型實參的類型,從而允許開發者不必顯式提供類型實參,下麵是以 maxGenerics
函數為例的類型實參推斷過程示意圖:
我們看到,當 maxGenerics
函數傳入的實際參數為 []int{…}
時,Go 編譯器會將其類型 []int
與泛型函數參數列表中對應參數的類型([]T
)作比較,並推斷出 T == int
這一結果。當然這個例子的推斷過程較為簡單,那些有難度的,甚至無法肉眼可見的就交給 Go 編譯器去處理吧,我們沒有必要過於深入。
不過,這個類型實參自動推斷有一個前提,你一定要記牢,那就是它必須是函數的參數列表中使用了的類型形參,否則就會像下麵的示例中的代碼,編譯器將報無法推斷類型實參的錯誤:
func foo[T comparable, E any](a int, s E) {
}
foo(5, "hello") // 編譯器錯誤:cannot infer T
在編譯器無法推斷出結果時,我們可以給予編譯器“部分提示”,比如既然編譯器無法推斷出 T
的實參類型,那我們就顯式告訴編譯器 T
的實參類型,即在泛型函數調用時,在類型實參列表中顯式傳入 T
的實參類型,但 E
的實參類型依然由編譯器自動推斷,示例代碼如下:
var s = "hello"
foo[int](5, s) //ok
foo[int,](5, s) //ok
那麼,除了函數參數列表中的參數類型可以作為類型實參推斷的依據外,函數返回值的類型是否也可以呢?我們看下麵示例:
func foo[T any](a int) T {
var zero T
return zero
}
var a int = foo(5) // 編譯器錯誤:cannot infer T
println(a)
我們看到,這個函數僅在返回值中使用了類型參數,但編譯器沒能推斷出 T 的類型,所以我們切記:不能通過返回值類型來推斷類型實參。
有了函數類型實參推斷後,在大多數情況下,我們調用泛型函數就無須顯式傳遞類型實參了,開發者也因此獲得了與普通函數調用幾乎一致的體驗。
其實泛型函數調用是一個不同於普通函數調用的過程,為了揭開其中的“奧秘”,接下來我們看看泛型函數調用過程究竟發生了什麼。
3.3 泛型函數實例化(instantiation)
我們還以 maxGenerics
為例來演示一下這個過程:
maxGenerics([]int{1, 2, -4, -6, 7, 0})
上面代碼是對 maxGenerics
泛型函數的一次調用,Go 對這段泛型函數調用代碼的處理分為兩個階段,如下圖所示:
我們看到,Go 首先會對泛型函數進行實例化(instantiation
),即根據自動推斷出的類型實參生成一個新函數(當然這一過程是在編譯階段完成的,不會對運行時性能產生影響),然後才會調用這個新函數對輸入的函數參數進行處理。
我們也可以用一種更形象的方式來描述上述泛型函數的實例化過程。實例化就好比一家生產“求最大值”機器的工廠,它會根據要比較大小的對象的類型將這樣的機器生產出來。以上面的例子來說,整個實例化過程如下:
- 工廠接單:調用
maxGenerics([]int{…})
,工廠師傅發現要比較大小的對象類型為int
; - 模具檢查與匹配:檢查
int
類型是否滿足模具的約束要求,即int
是否滿足ordered
約束,如滿足,則將其作為類型實參替換maxGenerics
函數中的類型形參T
,結果為maxGenerics[int]
; - 生產機器:將泛型函數
maxGenerics
實例化為一個新函數,這裡將其起名為maxGenericsInt
,其函數原型為func([]int) int
。本質上maxGenericsInt := maxGenerics[int]
。
我們實際的 Go 代碼也可以真實得到這台新生產出的“機器”,如下麵代碼所示:
maxGenericsInt := maxGenerics[int] // 實例化後得到的新“機器”:maxGenericsInt
fmt.Printf("%T\n", maxGenericsInt) // func([]int) int
一旦針對 int
對象的“求最大值”的機器被生產出來了,它就可以對目標對象進行處理了,這和普通的函數調用沒有區別。這裡就相當於調用如下代碼:
maxGenericsInt([]int{1, 2, -4, -6, 7, 0}) // 輸出:7
整個過程只需檢查傳入的函數實參([]int{1, 2, …}
)的類型與 maxGenericsInt
函數原型中的形參類型([]int
)是否匹配即可。
另外要註意,當我們使用相同類型實參對泛型函數進行多次調用時,Go 僅會做一次實例化,並復用實例化後的函數,比如:
maxGenerics([]int{1, 2, -4, -6, 7, 0})
maxGenerics([]int{11, 12, 14, -36,27, 0}) // 復用第一次調用後生成的原型為func([]int) int的函數
好了,接下來我們再來看 Go 對類型參數的另一類支持:帶有類型參數的自定義類型,即泛型類型。
五、泛型類型
5.1 聲明泛型類型
所謂泛型類型,就是在類型聲明中帶有類型參數的 Go 類型,比如下麵代碼中的 maxableSlice
:
// maxable_slice.go
type maxableSlice[T ordered] struct {
elems []T
}
顧名思義,maxableSlice
是一個自定義切片類型,這個類型的特點是總可以獲取其內部元素的最大值,其唯一的要求是其內部元素是可排序的,它通過帶有 ordered
約束的類型參數來明確這一要求。像這樣在定義中帶有類型參數的類型就被稱為泛型類型(generic type
)。
從例子中的 maxableSlice
類型聲明中我們可以看到,在泛型類型中,類型參數列表放在類型名字後面的方括弧中。和泛型函數一樣,泛型類型可以有多個類型參數,類型參數名通常是首字母大寫的,這些類型參數也必須是具名的,且命名唯一。其一般形式如下:
type TypeName[T1 constraint1, T2 constraint2, ..., Tn constraintN] TypeLiteral
和泛型函數中類型參數有其作用域一樣,泛型類型中類型參數的作用域範圍也是從類型參數列表左側的方括弧[
開始,一直持續到類型定義結束的位置,如下圖所示:
這樣的作用域將方便我們在各個欄位中靈活使用類型參數,下麵是一些自定義泛型類型的示例:
type Set[T comparable] map[T]struct{}
type sliceFn[T any] struct {
s []T
cmp func(T, T) bool
}
type Map[K, V any] struct {
root *node[K, V]
compare func(K, K) int
}
type element[T any] struct {
next *element[T]
val T
}
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~complex64 | ~complex128
}
type NumericAbs[T Numeric] interface {
Abs() T
}
我們看到,泛型類型中的類型參數可以用來作為類型聲明中欄位的類型(比如上面的 element
類型)、複合類型的元素類型(比如上面的 Set
和 Map
類型)或方法的參數和返回值類型(如 NumericAbs
介面類型)等。
如果要在泛型類型聲明的內部引用該類型名,必須要帶上類型參數,如上面的 element
結構體中的 next
欄位的類型:*element[T]
。按照泛型設計方案,如果泛型類型有不止一個類型參數,那麼在其聲明內部引用該類型名時,不僅要帶上所有類型參數,類型參數的順序也要與聲明中類型參數列表中的順序一致,比如:
type P[T1, T2 any] struct {
F *P[T1, T2] // ok
}
不過從實測結果來看,對於下麵不符合技術方案的泛型類型聲明也並未報錯:
type P[T1, T2 any] struct {
F *P[T2, T1] // 不符合技術方案,但Go 編譯器並未報錯
}
5.2 使用泛型類型
和泛型函數一樣,使用泛型類型時也會有一個實例化(instantiation
)過程,比如:
var sl = maxableSlice[int]{
elems: []int{1, 2, -4, -6, 7, 0},
}
Go 會根據傳入的類型實參(int
)生成一個新的類型並創建該類型的變數實例,sl
的類型等價於下麵代碼:
type maxableIntSlice struct {
elems []int
}
看到這裡你可能會問:泛型類型是否可以像泛型函數那樣實現類型實參的自動推斷呢?很遺憾,目前的 Go 1.21.4 尚不支持,下麵代碼會遭到 Go 編譯器的報錯:
var sl = maxableSlice {
elems: []int{1, 2, -4, -6, 7, 0}, // 編譯器錯誤:cannot use generic type maxableSlice[T ordered] without instantiation
}
不過這一特性在 Go 的未來版本中可能會得到支持。
既然涉及到了類型,你肯定會想到諸如類型別名、類型嵌入等 Go 語言機制,那麼這些語言機制對泛型類型的支持情況又是如何呢?我們逐一來看一下。
5.2.1 泛型類型與類型別名
我們知道類型別名type alias)與其綁定的原類型是完全等價的,但這僅限於原類型是一個直接類型,即可直接用於聲明變數的類型。那麼將類型別名與泛型類型綁定是否可行呢?我們來看一個示例:
type foo[T1 any, T2 comparable] struct {
a T1
b T2
}
type fooAlias = foo // 編譯器錯誤:cannot use generic type foo[T1 any, T2 comparable] without instantiation
在上述代碼中,我們為泛型類型 foo
建立了類型別名 fooAlias
,但編譯這段代碼時,編譯器還是報了錯誤!
這是因為,泛型類型只是一個生產真實類型的“工廠”,它自身在未實例化之前是不能直接用於聲明變數的,因此不符合類型別名機制的要求。泛型類型只有實例化後才能得到一個真實類型,例如下麵的代碼就是合法的:
type fooAlias = foo[int, string]
也就是說,我們只能為泛型類型實例化後的類型創建類型別名,實際上上述 fooAlias
等價於實例化後的類型 fooInstantiation
:
type fooInstantiation struct {
a int
b string
}
5.2.2 泛型類型與類型嵌入
類型嵌入是運用 Go 組合設計哲學的一個重要手段。引入泛型類型之後,我們依然可以在泛型類型定義中嵌入普通類型,比如下麵示例中 Lockable
類型中嵌入的 sync.Mutex
:
type Lockable[T any] struct {
t T
sync.Mutex
}
func (l *Lockable[T]) Get() T {
l.Lock()
defer l.Unlock()
return l.t
}
func (l *Lockable[T]) Set(v T) {
l.Lock()
defer l.Unlock()
l.t = v
}
在泛型類型定義中,我們也可以將其他泛型類型實例化後的類型作為成員。現在我們改寫一下上面的 Lockable
,為其嵌入另外一個泛型類型實例化後的類型 Slice[int]
:
type Slice[T any] []T
func (s Slice[T]) String() string {
if len(s) == 0 {
return ""
}
var result = fmt.Sprintf("%v", s[0])
for _, v := range s[1:] {
result = fmt.Sprintf("%v, %v", result, v)
}
return result
}
type Lockable[T any] struct {
t T
Slice[int]
sync.Mutex
}
func main() {
n := Lockable[string]{
t: "hello",
Slice: []int{1, 2, 3},
}
println(n.String()) // 輸出:1, 2, 3
}
我們看到,代碼使用泛型類型名(Slice
)作為嵌入後的欄位名,並且 Slice[int]
的方法 String
被提升為 Lockable
實例化後的類型的方法了。同理,在普通類型定義中,我們也可以使用實例化後的泛型類型作為成員,比如讓上面的 Slice[int]
嵌入到一個普通類型 Foo
中,示例代碼如下:
type Foo struct {
Slice[int]
}
func main() {
f := Foo{
Slice: []int{1, 2, 3},
}
println(f.String()) // 輸出:1, 2, 3
}
此外,Go 泛型設計方案支持在泛型類型定義中嵌入類型參數作為成員,比如下麵的泛型類型 Lockable
內嵌了一個類型 T
,且 T
恰為其類型參數:
type Lockable[T any] struct {
T
sync.Mutex
}
不過,Go 最新版1.21.4
編譯上述代碼時會針對嵌入 T 的那一行報如下錯誤:
編譯器報錯:embedded field type cannot be a (pointer to a) type parameter
關於這個錯誤,Go 官方在其 issue 中給出了臨時的結論:暫不支持。
六、泛型方法
我們知道 Go 類型可以擁有自己的方法(method
),泛型類型也不例外,為泛型類型定義的方法稱為泛型方法(generic method
),接下來我們就來看看如何定義和使用泛型方法。
我們用一個示例,給 maxableSlice
泛型類型定義 max
方法,看一下泛型方法的結構:
func (sl *maxableSlice[T]) max() T {
if len(sl.elems) == 0 {
panic("slice is empty")
}
max := sl.elems[0]
for _, v := range sl.elems[1:] {
if v > max {
max = v
}
}
return max
}
我們看到,在定義泛型類型的方法時,方法的 receiver
部分不僅要帶上類型名稱,還需要帶上完整的類型形參列表(如 maxableSlice[T]
),這些類型形參後續可以用在方法的參數列表和返回值列表中。
不過在 Go 泛型目前的設計中,泛型方法自身不可以再支持類型參數了,不能像下麵這樣定義泛型方法:
func (f *foo[T]) M1[E any](e E) T { // 編譯器錯誤:syntax error: method must have no type parameters
//... ...
}
關於泛型方法未來是否能支持類型參數,目前 Go 團隊傾向於否,但最終結果 Go 團隊還要根據 Go 社區在使用泛型過程中的反饋而定。
在泛型方法中,receiver
中某個類型參數如果沒有在方法參數列表和返回值中使用,可以用“_”代替,但不能不寫,比如:
type foo[A comparable, B any] struct{}
func (foo[A, B]) M1() { // ok
}
或
func (foo[_, _]) M1() { // ok
}
或
func (foo[A, _]) M1() { // ok
}
但
func (foo[]) M1() { // 錯誤:receiver部分缺少類型參數
}
另外,泛型方法中的 receiver
中類型參數名字可以與泛型類型中的類型形參名字不同,位置和數量對上即可。我們還以上面的泛型類型 foo
為例,可以為它添加下麵方法:
type foo[A comparable, B any] struct{}
func (foo[First, Second]) M1(a First, b Second) { // First對應類型參數A,Second對應類型參數B
}
分享是一種快樂,開心是一種態度!