Go 語言設(shè)計(jì)者 Robert Griesemer 深入介紹泛型
Go 官方博客近日發(fā)表了一篇介紹新特性“泛型”的文章,作者是兩位重量級人物 —— Robert Griesemer 和 Ian Lance Taylor,內(nèi)容基于他們在 2021 年 GopherCon 大會上的演講。

不久前正式發(fā)布的 Go 1.18 添加了對泛型的支持,據(jù)稱泛型是 Go 開源以來所做的最大改變。泛型是一種編程范式,這種范式獨(dú)立于所使用的特定類型,泛型允許在函數(shù)和類型的實(shí)現(xiàn)中使用某個類型集合中的任何一種類型。泛型為 Go 添加了三個新的重要內(nèi)容:
面向函數(shù)和類型的“類型形參” (type parameters)
將接口類型定義為類型集合,包括沒有方法的接口類型(type sets)
類型推斷:在大多數(shù)情況下,在調(diào)用泛型函數(shù)時可省略“類型實(shí)參” (type arguments)
Type Parameters
現(xiàn)在函數(shù)和類型都具有類型形參” (type parameters),類型形參列表看起來就是一個普通的參數(shù)列表,除了它使用的是方括號而不是小括號。
先看一下基本的非泛型函數(shù):
func?Min(x,?y?float64)?float64?{
????if?x?????????return?x
????}
????return?y
}
通過添加類型形參列表來使這個函數(shù)泛型化——使其適用于不同的類型。在此示例中,添加了一個帶有單個類型形參T的類型參數(shù)列表,并替換了 float64。
import?"golang.org/x/exp/constraints"
func?GMin[T?constraints.Ordered](x,?y?T)?T?{
????if?x?????????return?x
????}
????return?y
}
使用類型實(shí)參調(diào)用泛型函數(shù):
x?:=?GMin[int](2,?3)
使用類型參數(shù)調(diào)用 GMin ,int 的作用稱為實(shí)例化。編譯中這個過程分為兩個步驟:1. 編譯器在泛型函數(shù)或泛型類型中用所有類型形參替換它們各自的類型實(shí)參;2. 編譯器驗(yàn)證每個類型形參是否滿足各自的約束。如果第二步失敗,實(shí)例化就會失敗。成功實(shí)例化后,非泛型函數(shù)就生成了,與其他普通函數(shù)的調(diào)用方式一樣:
fmin?:=?GMin[float64]
m?:=?fmin(2.71,?3.14)
實(shí)例化 Gmin[float64] 會生成與普通的 Min(x,yfloat64)功能一樣的函數(shù)。
類型參數(shù)也可以與 type 一起使用。
type?Tree[T?interface{}]?struct?{
????left,?right?*Tree[T]
????value???????T
}
func?(t?*Tree[T])?Lookup(x?T)?*Tree[T]?{?...?}
var?stringTree?Tree[string]
在上面的例子中,泛型 type Tree 存儲了類型形參 T 的值。泛型類型也可以有方法,比如本例中的 Lookup。為了使用泛型類型,它必須被實(shí)例化;Tree[string] 就是是使用類型實(shí)參 string 來實(shí)例化 Tree類型。
Type sets
普通函數(shù)的每個形參都有一個類型;該類型定義了一組值。例如,如果我們有一個 float64 類型,就像上面非泛型函數(shù) Min 中那樣,允許的實(shí)參是由 float64 類型表示的浮點(diǎn)值。
同樣,類型參數(shù)列表具有每個類型參數(shù)的類型(讀起來有點(diǎn)繞)。但正是因?yàn)轭愋蛥?shù)本身是一種類型,所以類型形參的類型定義了一組類型。這個元類型稱為類型約束。
在泛型函數(shù) GMin 中,類型約束是從包 constraints[1] 中導(dǎo)入的。Ordered 約束描述了所有類型的集合,這些類型的值可以被排序,或者換句話說,與這些類型可以使用操作符 <,<=,>,等比較。約束確保只有具有可排序值的類型才能傳遞給 GMin。這也意味著在 GMin函數(shù)體中,可以使用該類型形參的值與<操作符進(jìn)行比較。
在 Go 中,類型約束必須是接口。也就是說,接口類型可以用作值類型,也可以用作元類型(meta-type)。接口定義了方法,因此顯然我們可以表達(dá)需要特定方法出現(xiàn)的類型約束。但約束 Ordered 也是接口類型。
我們可以換個角度來理解 inerface。
方法集可以看出,接口定義了一組類型集合,即實(shí)現(xiàn)這些方法的類型。從這個角度來看,作為接口類型集合元素的任何類型都實(shí)現(xiàn)了接口。

從這兩個圖中我們可以得出一個結(jié)論,對于每一組方法,我們可以想象成實(shí)現(xiàn)這些方法的相應(yīng)類型集合,這就是接口定義的類型集。但是,以“類型”的方式操作比方法設(shè)置具有優(yōu)勢:我們可以將類型添加到集合中,以一種新的方法控制它們。為此,我們擴(kuò)展了接口類型的語法。例如,interface{int|string|bool} 定義了包含 int、string 和 bool 類型的類型集合。

也可以說這個接口只對 int、string 或 bool 類型生效。
此時 contraints.Ordered 的實(shí)際定義是:
type?Ordered?interface?{
????Integer|Float|~string
}
這個定義的含義是,Ordered interface是所有 int、float 和 string 類型的集合。| 表示類型的并集(在本例中為類型集合)。Integer 和 Float 是在約束包中類似定義的接口類型。注意,Ordered 接口沒有定義方法。
對于類型約束,我們通常不關(guān)心特定的類型,比如string;我們對所有字符串類型都感興趣。這就是~標(biāo)記的作用。表達(dá)式 ~string 表示基礎(chǔ)類型為 string 的所有類型的集合。這包括類型字符串本身以及用定義聲明的所有類型,如 type MyString string。
當(dāng)然,我們?nèi)匀幌M诮涌谥兄付ǚ椒?,并且希望向后兼容。?Go 1.18 中,接口可以像以前一樣包含方法和嵌入接口,但它也可以嵌入非接口類型、聯(lián)合和底層類型集。
用作約束的接口可以指定名稱(如Ordered),也可以直接內(nèi)聯(lián)在類型參數(shù)列表中使用。例如:
[S?interface{~[]E},?E?interface{}]
這里 S 必須是一個切片類型,其元素類型可以是任何類型。
因?yàn)檫@是一種很常用的使用方式,對于處于約束位置的接口,我們可以寫成:
[S?~[]E,?E?interface{}]
因?yàn)榭?interface 在類型參數(shù)列表和普通的 Go 代碼中都很常見,所以 Go 1.18引入了一個新的預(yù)先聲明的標(biāo)識符 any,作為空接口類型的別名。這樣我們就可以這樣寫:
[S?~[]E,?E?any]
接口作為類型集是一種強(qiáng)大的新機(jī)制,是類型約束的關(guān)鍵技術(shù)。
Type inference
最后一個新的主要語言特性是類型推斷。這是為了支持泛型而做的最復(fù)雜的工作,但它可以支持開發(fā)者以最自然的方式編寫調(diào)用泛型函數(shù)的代碼。
函數(shù)參數(shù)類型推斷 (Function argument type inference)
對于類型參數(shù),需要傳遞類型參數(shù),這會導(dǎo)致代碼冗長?;氐轿覀兊姆盒?GMin 函數(shù):
func?GMin[T?constraints.Ordered](x,?y?T)?T?{?...?}
類型形參 T 用于指定普通的非類型實(shí)參 x 和 y 的類型。正如我們前面看到的,這可以用顯式類型實(shí)參調(diào)用:
var?a,?b,?m?float64
m?=?GMin[float64](a,?b)?//?explicit?type?argument
在許多情況下,編譯器可以從普通參數(shù)推斷出T的類型參數(shù)。這使得代碼更短,同時保持清晰:
var?a,?b,?m?float64
m?=?GMin(a,?b)?//?no?type?argument
這是通過將參數(shù) a 和 b 的類型與參數(shù) x 和 y 的類型進(jìn)行推導(dǎo)匹配來實(shí)現(xiàn)的。
這種從函數(shù)的實(shí)參類型推斷出類型參數(shù)的方式稱為函數(shù)參數(shù)類型推斷。
另外,類型推導(dǎo)只對函數(shù)形參生效,函數(shù)體或者返回值的類型無法推斷,比如 MakeT[T any]() T 這樣的函數(shù),該函數(shù)只使用 T 作為返回值。
約束類型推斷 (Constraint type inference)
另一種類型推斷是約束類型推斷。看一下下面這個例子:
//?Scale?returns?a?copy?of?s?with?each?element?multiplied?by?c.
//?This?implementation?has?a?problem,?as?we?will?see.
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
}
這是一個泛型函數(shù),適用于任何整數(shù)類型的切片。
現(xiàn)在假設(shè)我們有一個多維的 Point 類型,其中每個 Point 只是一個給出點(diǎn)坐標(biāo)的整數(shù)列表。
type?Point?[]int32
func?(p?Point)?String()?string?{
????//?Details?not?important.
}
如果我們想要縮放一個點(diǎn),因?yàn)?Point 只是一個整數(shù)的切片,所以我們可以使用之前編寫的 Scale 泛型函數(shù)。
//?ScaleAndPrint?doubles?a?Point?and?prints?it.
func?ScaleAndPrint(p?Point)?{
????r?:=?Scale(p,?2)
????fmt.Println(r.String())?//?DOES?NOT?COMPILE
}
但是這段代碼不能編譯通過,會報(bào) r.String undefined (type []int32 has no field or method String) ?的錯誤。
問題在于 Scale 函數(shù)返回一個類型為 []E 的值,其中 E 是參數(shù) slice 的元素類型。當(dāng)使用基礎(chǔ)類型為 []int32 的 Point 類型值調(diào)用 Scale 時,返回的值為 []int32 類型,而不是 Point 類型。
為了解決這個問題,我們使用類型參數(shù)作為切片的類型,修改 Scale 函數(shù)。
//?Scale?returns?a?copy?of?s?with?each?element?multiplied?by?c.
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
}
我們引入了一個新的類型參數(shù) S,它是 slice 參數(shù)的類型。并對它進(jìn)行了約束,使其基礎(chǔ)類型是 S 而不是 []E ,返回值類型也是S。因?yàn)?E 被約束為整數(shù),所以效果與之前相同:第一個參數(shù)必須是某個整數(shù)類型的切片。
所以,約束類型推斷從類型參數(shù)約束推導(dǎo)類型參數(shù)。當(dāng)一個類型參數(shù)有一個根據(jù)另一個類型參數(shù)定義的約束時,就使用它。當(dāng)其中一個類型參數(shù)的類型已知時,就使用約束來推斷另一個類型參數(shù)(這里譯者水平有限,感覺不能講出作者的本意,可以閱讀 proposal document[2] 和 language spec[3] 來深入了解)。
Type inference in practice
雖然類型推斷的工作原理細(xì)節(jié)很復(fù)雜,但使用起來比較簡單:類型推斷要么成功,要么失敗。如果它成功,類型實(shí)參可以被省略,泛型函數(shù)的調(diào)用與普通函數(shù)是一樣的。如果類型推斷失敗,編譯器將會報(bào)錯,在這種情況下,只需要提供必要的類型實(shí)參。
Conclusion
泛型是 Go 1.18 的最重要語言特性,Robert Griesemer 和 Ian Lance Taylor 表示,這個功能實(shí)現(xiàn)得很好并且質(zhì)量很高。雖然他們鼓勵在有必要的場景中使用泛型以減少冗余代碼,但在生產(chǎn)環(huán)境中部署泛型代碼時,還是要謹(jǐn)慎小心。
原文地址[4]
參考資料
[1]package constraints: https://golang.org/x/exp/constraints
[2]proposal docs: https://go.googlesource.com/proposal/+/HEAD/design/43651-type-parameters.md
[3]language spec: https://go.dev/ref/spec
[4]原文地址: https://go.dev/blog/intro-generics
? ? ? ? ? ? 官方資訊*最新技術(shù)*獨(dú)家解讀
