在我們選擇的編程語言中,我們多長時間會經(jīng)歷一次根本性的變化?有些語言會變化得更頻繁一些,但還有些語言會比溫布爾登更保守。 Go 語言就屬于后者。有時對我來說它實在太古板了?!癎o 不是這么寫的!”是我夢到最多的一句話,。Go 的多數(shù)新版本都只是對已有方向循序漸進(jìn)的改善,。 一開始,我并不覺得自己喜歡這樣的路徑,。沒什么新鮮事物刺激的話,,總是用一種工具遲早會令人厭煩的。有時我寧愿看無聊的《與卡戴珊姐妹同行》也不想碰 Go 了,。 (開個玩笑,。我沒裝電視的一個原因就是想逃離那些可能污染我美麗眼球的電視節(jié)目。) 然后……新鮮血液終于來了,。去年底,,Go 團(tuán)隊宣布 1.18 版開始支持泛型,,這可不是以前那種小打小鬧的改進(jìn),也不是什么對開發(fā)人員行為絮絮叨叨的建議和約束,。 打起精神來吧,,革命來臨了。 那么,,什么是泛型,?
不談什么復(fù)雜的定義,,我們來看看真實的例子——下面的代碼中,泛型讓我們得以避開許多 Max 或 Min 函數(shù),,而是寫成: func MaxInt(a, b int) int { // some code}func MaxFloat64(a, b float64) float64 { // some code}func MaxByte(a, b byte) byte { // some code} 復(fù)制代碼 只聲明一個方法,,如下所示:
復(fù)制代碼 等等,剛剛發(fā)生了什么,?其實我們沒有在 Go 中為每種類型都定義一個方法,,而是使用了泛型——我們使用泛型類型,參數(shù) T 作為這個方法的參數(shù),。通過這個小小的調(diào)整,,我們就能支持所有 orderable 的類型。參數(shù) T 代表滿足 Ordered 約束的任何類型(稍后我們將討論約束主題),。所以,,一開始我們需要定義 T 是什么類型。 接下來,,我們定義要在何處使用這個參數(shù)化類型,。這里,我們確定輸入和輸出參數(shù)都是 T 類型,。如果我們將 T 定義為整數(shù)來執(zhí)行方法,,那么這里的所有內(nèi)容都是整數(shù): func main() { fmt.Println(Max[int](1, 2))}//// this code behaves exactly like method:// Max(a, b int) int 復(fù)制代碼 能做的不僅是這些。我們可以提供盡可能多的參數(shù)化類型,。我們可以將它們分配給不同的輸入和輸出參數(shù),,隨我們的喜好:
復(fù)制代碼 這里我們有三個參數(shù),R,、S 和 T,。正如我們從約束 any 中看到的那樣(其行為類似于 interface{}),,這些類型可以是任何東西。所以現(xiàn)在我們應(yīng)該清楚了什么是泛型,,以及我們?nèi)绾卧?Go 中使用它們了,。下面我們來談?wù)勊鼛淼募尤诵牡挠绊憽?/span> 如何在本地環(huán)境中啟用泛型?目前 Go 1.18 的穩(wěn)定版本尚未發(fā)布,。因此我們需要做一些調(diào)整來在本地對其進(jìn)行測試,。 為了啟用泛型,我使用了 Jetbrains 的 Goland,。我在他們的網(wǎng)站上找到了一篇有用的文章,,用于設(shè)置在 Goland 中運行代碼的環(huán)境。 與那篇文章的唯一區(qū)別是我使用了帶有 master 分支的 Go 源代碼( 在 master 分支上,我們可以享用來自標(biāo)準(zhǔn) Go 庫的新包,,Constraints,。 速度,我要的是速度
當(dāng)然,,泛型并不像反射,,它也沒打算做成那樣。不過至少在某些用例中,,泛型是生成代碼的一種替代方法,。 因此,這意味著我們想看到的是基于泛型的代碼與“經(jīng)典”執(zhí)行的代碼具有相同的基準(zhǔn)測試結(jié)果,。我們來檢查一個基本案例: package mainimport ( 'constraints' 'fmt')type Number interface { constraints.Integer | constraints.Float}func Transform[S Number, T Number](input []S) []T { output := make([]T, 0, len(input)) for _, v := range input { output = append(output, T(v)) } return output}func main() { fmt.Printf('%#v', Transform[int, float64]([]int{1, 2, 3, 6}))}////// Out:// []float64{1, 2, 3, 6} 復(fù)制代碼 這里是將一種 Number 類型轉(zhuǎn)換為另一種的小方法,。Number 是我們基于 Go 標(biāo)準(zhǔn)庫中的 Integer 和 Float 約束構(gòu)建的約束(我們稍后將討論這個主題)。Number 可以是 Go 中的任何數(shù)值類型:從 int 的任何衍生到 uint,、float 等等,。方法 Trasforms 會以第一個參數(shù)化數(shù)值類型 S 作為切片基數(shù)的切片,并將其轉(zhuǎn)換為以第二個參數(shù)化數(shù)字類型 T 作為切片基數(shù)的切片,。 簡而言之,,如果我們想將一個整數(shù)切片轉(zhuǎn)換成一個浮點切片,我們會像在 main 函數(shù)中所做的那樣調(diào)用這個方法。 我們函數(shù)的非泛型替代方法需要一個整數(shù)切片并返回一個浮點切片,。因此,,這就是我們將在基準(zhǔn)測試中測試的內(nèi)容:
復(fù)制代碼 并沒有驚喜。兩種方法的執(zhí)行時間幾乎一樣,,也就是說使用泛型不會影響我們應(yīng)用程序的性能,。但是它對結(jié)構(gòu)(struct)有影響嗎?我們嘗試一下?,F(xiàn)在,,我們將使用結(jié)構(gòu)并將方法附加到它們上。測試任務(wù)沒變——將一個切片轉(zhuǎn)換為另一個切片: type Transformer[S Number, T Number] struct { slice []S}func (t *Transformer[S, T]) Do() []T { output := make([]T, 0, len(t.slice)) for _, v := range t.slice { output = append(output, T(v)) } return output}func BenchmarkGenerics(b *testing.B) { for i := 0; i < b.N; i++ { object := Transformer[int, float64]{ slice: []int{1, 2, 3, 6}, } object.Do() }}type TransformerClassic struct { slice []int}func (t *TransformerClassic) Do() []float64 { output := make([]float64, 0, len(t.slice)) for _, v := range t.slice { output = append(output, float64(v)) } return output}func BenchmarkClassic(b *testing.B) { for i := 0; i < b.N; i++ { object := TransformerClassic{ slice: []int{1, 2, 3, 6}, } object.Do() }}////// Out:// goos: darwin// goarch: amd64// pkg: test/generics// cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz//// first run:// BenchmarkGenerics// BenchmarkGenerics-8 29744370 33.81 ns/op// BenchmarkClassic// BenchmarkClassic-8 36323090 31.51 ns/op// PASS//// second run:// BenchmarkGenerics// BenchmarkGenerics-8 35238153 32.11 ns/op// BenchmarkClassic// BenchmarkClassic-8 37007353 31.80 ns/op// PASS//// third run:// BenchmarkGenerics// BenchmarkGenerics-8 34512194 33.12 ns/op// BenchmarkClassic// BenchmarkClassic-8 35426551 32.44 ns/op// PASS 復(fù)制代碼 依舊沒有驚喜,。不管使用泛型還是經(jīng)典實現(xiàn)都不會對 Go 代碼的性能帶來任何影響,。是的,我們的確沒有測試太復(fù)雜的用例,,但如果有顯著差異我們肯定已經(jīng)看到了才對,。所以,我們可以安心了,。 約束
復(fù)制代碼 除了我們的方法 Max 不計算其輸入的最大值而是將它們都返回之外,上面的示例并沒有什么奇怪的地方,。為此,,我們使用一個定義為 interface{}的參數(shù)化類型 T。在這個示例中,,我們不應(yīng)將 interface{}視為一種類型,,而應(yīng)將其視為一種約束。我們使用約束來為我們的參數(shù)化類型定義規(guī)則,,并為 Go 編譯器提供一些關(guān)于期望的背景知識,。 重復(fù)一遍:我們在這里不使用 interface{}作為類型,而是作為約束,。我們?yōu)閰?shù)化類型定義各種規(guī)則,,在這個例子中該類型必須支持 interface{}所做的任何事情。所以實際上,,我們也可以在這里使用 any 約束,。 (老實說,在所有示例中,,我更喜歡 interface{}而不是 any,,因為我的 Goland IDE 不支持新的保留字(any、comparable),,然后我的 IDE 中出現(xiàn)了大量錯誤消息,,自動完成也不能用了,。) 在編譯時,編譯器可以接受一個約束,,并使用它來檢查參數(shù)化類型是否支持我們想要在以下代碼中執(zhí)行的運算符和方法,。 由于編譯器在運行時進(jìn)行大部分優(yōu)化工作(因此我們就不會影響運行時了,正如我們在基準(zhǔn)測試中看到的那樣),,它只允許為特定約束定義的運算符和函數(shù),。 因此,要了解約束的重要性,,我們來完成 Max 方法的實現(xiàn)并嘗試比較 a 和 b 變量: func Max[T any](a, b T) T { if a > b { return a } return b}func main() { fmt.Println(Max(1, 2)) fmt.Println(Max(3.0, 2.0))}////// Out:// ./main.go:6:5: invalid operation: cannot compare a > b (operator > not defined on T) 復(fù)制代碼 當(dāng)我們嘗試觸發(fā)這個應(yīng)用程序時得到一個錯誤——operator>not defined on T,。因為我們將 T 類型定義為 any,所以最終類型可以是任何東西,。從這里開始,,編譯器就不知道如何處理這個運算符了。為了解決這個問題,,我們需要將參數(shù)化類型 T 定義為允許這種運算符的某種約束,。感謝 Go 團(tuán)隊的出色表現(xiàn),我們已經(jīng)有了 Constraints 包,,它就有這樣的約束,。 我們要使用的約束名為 Ordered,調(diào)整后的代碼如此優(yōu)雅:
復(fù)制代碼 通過使用 Ordered 約束,,我們得到了結(jié)果,。這個例子的好處是我們可以看到編譯器如何解釋最終類型 T,這取決于我們傳遞給方法的值,。我們無需在方括號中定義實際類型,,就像前兩種情況一樣,編譯器可以識別用于參數(shù)的類型——在 Go 中應(yīng)該是 int 和 float64,。 另一方面,,如果我們想使用某些不是默認(rèn)的類型,比如 int64 或 float32,,就應(yīng)該嚴(yán)格在方括號中傳遞這些類型,。然后我們確切地編譯器具體該做什么。 如果需要,,我們可以擴(kuò)展函數(shù) Max 中的功能以支持在數(shù)組中搜索最大值: func Max[T constraints.Ordered](a []T) (T, error) { if len(a) == 0 { return T(0), errors.New('empty array') } max := a[0] for i := 1; i < len(a); i++ { if a[i] > max { max = a[i] } } return max, nil}func main() { fmt.Println(Max([]string{})) fmt.Println(Max([]string{'z', 'a', 'f'})) fmt.Println(Max([]int{1, 2, 5, 3})) fmt.Println(Max([]float32{4.0, 5.0, 2.0})) fmt.Println(Max([]float32{}))}////// Out:// empty array// z <nil>// 5 <nil>// 5 <nil>// 0 empty array 復(fù)制代碼 在這個例子中我們可以看到兩個有趣的點:
如果我們想使用運算符==,,可以使用一個新的保留字 comparable,這是一個僅支持此類運算符的唯一約束:
復(fù)制代碼 在上面的示例中,,我們可以看到 comparable 約束的用法應(yīng)該是什么樣的,。同樣,即使沒有在方括號中嚴(yán)格定義它們,,編譯器也可以識別實際類型,。示例中要提到的一點是,我們在兩種不同的方法 Equal 和 Dummy 中為兩種參數(shù)化類型使用了相同的字母 T,。 每個 T 類型僅在這個方法的作用域(或結(jié)構(gòu)及其方法)中定義,,我們不會在其作用域之外談?wù)撓嗤?T 類型。我們可以用不同的方法重復(fù)同一個字母,,類型仍然是相互獨立的,。 自定義約束我們可以自定義約束,這很容易,。約束可以是我們想要的任何類型,,但最好的選擇可能是使用接口: type Greeter interface { Greet()}func Greetings[T Greeter](t T) { t.Greet()}type EnglishGreeter struct{}func (g EnglishGreeter) Greet() { fmt.Println('Hello!')}type GermanGreeter struct{}func (g GermanGreeter) Greet() { fmt.Println('Hallo!')}func main() { Greetings(EnglishGreeter{}) Greetings(GermanGreeter{})}////// Out:// Hello!// Hallo! 復(fù)制代碼 我們定義了一個 Greeter 接口,,以便將它用作 Greetings 方法中的約束,。不是為了演示的話,這里我們可以直接使用 Greeter 類型的變量而不是泛型,。 類型集
這一重大變更為我們帶來了很多新的可能性:我們的接口類型也可以嵌入原始類型,如 int,、float64,、byte 而不僅僅是其他接口。這個特性使我們能夠定義更靈活的約束。 檢查以下示例:
復(fù)制代碼 我們定義了 Comparable 約束,,而且那種類型看起來有點奇怪,,對吧?Go 中使用類型集的新方法允許我們定義一個應(yīng)該是類型聯(lián)合的接口,。為了描述兩種類型之間的聯(lián)合,,我們應(yīng)該將它們放在接口中,并在它們之間放置一個運算符:|,。 因此在我們的示例中,,Comparable 接口是以下類型的聯(lián)合:rune、float64 和……我猜是 int,?是的,,它確實是 int,但這里定義為一個近似元素,。 正如你在類型集的提案中看到的那樣,,一個近似元素 T 的類型集是類型 T 和所有基礎(chǔ)類型為 T 的類型的類型集。 因此,,僅僅因為我們使用了~int 近似元素,,我們就可以將 customInt 類型的變量提供給 Compare 方法。如你所見,,我們將 customInt 定義為自定義類型,,其中 int 是底層類型。 如果我們沒有添加操作符~,,編譯器就會抱怨,,不會執(zhí)行應(yīng)用程序。 我們能走多遠(yuǎn),?
從標(biāo)準(zhǔn)庫開始,,我已經(jīng)可以看到許多代碼會在未來的版本中被重構(gòu),,轉(zhuǎn)而使用泛型。泛型甚至可能推動一些 ORM 的發(fā)展,,例如我們在Doctrine中看到的一樣,。 例如,,考慮一個來自Gorm包的模型: type ProductGorm struct { gorm.Model Name string Price uint}type UserGorm struct { gorm.Model FirstName string LastName string} 復(fù)制代碼 想象一下,我們想在 Go 中為兩個模型(ProductGorm 和 UserGorm)實現(xiàn)存儲庫模式,。在當(dāng)前的穩(wěn)定版本的 Go 中,,我們只能選擇以下某種解決方案:
復(fù)制代碼 所以,,我們有了 Repository 結(jié)構(gòu),它有一個參數(shù)化類型 T,,可以是任何東西,。請注意,我們僅在 Repository 類型定義中定義了 T,,并且只是將其分配的函數(shù)傳遞給它,。這里我們只能看到 Create 和 Get 兩個方法,只是為了演示而已,。為了讓演示更簡單一些,,我們創(chuàng)建兩個單獨的方法來初始化不同的 Repositories: func NewProductRepository(db *gorm.DB) *Repository[ProductGorm] { db.AutoMigrate(&ProductGorm{}) return &Repository[ProductGorm]{ db: db, }}func NewUserRepository(db *gorm.DB) *Repository[UserGorm] { db.AutoMigrate(&UserGorm{}) return &Repository[UserGorm]{ db: db, }} 復(fù)制代碼 這兩種方法返回具有預(yù)定義類型的存儲庫實例。下面對這個小應(yīng)用程序進(jìn)行最終測試:
復(fù)制代碼 是的,,它能行,。一種 Repository 的實現(xiàn),支持兩種模型,。零反射,,零代碼生成。我以為我永遠(yuǎn)不會在 Go 中看到這樣的東西,。我太高興了,,眼淚都快流出來了 總結(jié)毫無疑問,Go 中的泛型是一個巨大的變化,,可以迅速改變 Go 的使用方式,,而且很快會在 Go 社區(qū)中引發(fā)許多重構(gòu)。 雖然我?guī)缀趺刻於荚谕娣盒?,想看看我們還能用它做什么好東西,,但我也迫不及待地想在穩(wěn)定的 Go 版本中看到它們,。革命萬歲,! 原文鏈接: |
|