黃石網(wǎng)站制作上海推廣系統(tǒng)
類型推導(dǎo)和泛型
就像在使用??:=?
?時(shí)支持類型推導(dǎo)一樣,在調(diào)用泛型函數(shù)時(shí)Go同樣支持類型推導(dǎo)。可在上面對(duì)??Map?
?、??Filter?
?和??Reduce?
?調(diào)用中看出。有些場(chǎng)景無法進(jìn)行類型推導(dǎo)(如類型參數(shù)僅用作返回值)。這時(shí),必須指定所有的參數(shù)類型。下面的代碼演示了無法進(jìn)行類型推導(dǎo)的場(chǎng)景:
type Integer interface {int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}func Convert[T1, T2 Integer](in T1) T2 {return T2(in)
}func main() {var a int = 10b := Convert[int, int64](a) // 無法推導(dǎo)返回類型fmt.Println(b)
}
可在??The Go Playground???或??第8章的GitHub代碼庫??的sample_code/type_inference目錄下測(cè)試這段代碼。
類型元素限定常量
類型元素也可指定哪些常量可賦值給泛型變量。和運(yùn)算符一樣,常需要對(duì)類型元素中的所有類型名有效。沒有常量可同時(shí)賦值給??Ordered?
?中列出的所有類型,因此無法將一個(gè)常量賦值給該泛型類型的變量。如果使用??Integer?
?接口,以下代碼無法編譯通過,因?yàn)椴荒軐?,000賦值給8位的整型:
// INVALID!
func PlusOneThousand[T Integer](in T) T {return in + 1_000
}
但下面的就是有效的:
// VALID
func PlusOneHundred[T Integer](in T) T {return in + 100
}
組合泛型函數(shù)和泛型數(shù)據(jù)結(jié)構(gòu)
回到二叉樹示例,來看如何使用所學(xué)的知識(shí)生成適用所有實(shí)體類型的樹。
核心在于理解該樹需要一個(gè)泛型函數(shù),可比較兩個(gè)值給出排序:
type OrderableFunc [T any] func(t1, t2 T) int
有了??OrderableFunc?
?,我們就可以稍稍修改樹的實(shí)現(xiàn)。首先將其分成兩種類型,??Tree?
?和??Node?
?:
type Tree[T any] struct {f OrderableFunc[T]root *Node[T]
}type Node[T any] struct {val Tleft, right *Node[T]
}
通過構(gòu)造函數(shù)構(gòu)造一個(gè)新??Tree?
?:
func NewTree[T any](f OrderableFunc[T]) *Tree[T] {return &Tree[T]{f: f,}
}
??Tree?
?的方法非常簡(jiǎn)單,因?yàn)樗{(diào)用??Node?
?來完成任務(wù):
func (t *Tree[T]) Add(v T) {t.root = t.root.Add(t.f, v)
}func (t *Tree[T]) Contains(v T) bool {return t.root.Contains(t.f, v)
}
??Node?
?的??Add?
?和??Contains?
?方法與之前的非常類似。唯一的區(qū)別是傳遞了用于排序元素的函數(shù):
func (n *Node[T]) Add(f OrderableFunc[T], v T) *Node[T] {if n == nil {return &Node[T]{val: v}}switch r := f(v, n.val); {case r <= -1:n.left = n.left.Add(f, v)case r >= 1:n.right = n.right.Add(f, v)}return n
}func (n *Node[T]) Contains(f OrderableFunc[T], v T) bool {if n == nil {return false}switch r := f(v, n.val); {case r <= -1:return n.left.Contains(f, v)case r >= 1:return n.right.Contains(f, v)}return true
}
現(xiàn)在我們需要匹配??OrderedFunc?
?定義的函數(shù)。所幸我們已經(jīng)見過一個(gè):??cmp?
?包中的??Compare?
?。在對(duì)??Tree?
?使用它時(shí)是這樣:
t1 := NewTree(cmp.Compare[int])
t1.Add(10)
t1.Add(30)
t1.Add(15)
fmt.Println(t1.Contains(15))
fmt.Println(t1.Contains(40))
對(duì)于結(jié)構(gòu)體,有兩種選項(xiàng)??梢跃帉懸粋€(gè)函數(shù):
type Person struct {Name stringAge int
}func OrderPeople(p1, p2 Person) int {out := cmp.Compare(p1.Name, p2.Name)if out == 0 {out = cmp.Compare(p1.Age, p2.Age)}return out
}
然后在創(chuàng)建樹進(jìn)傳遞該函數(shù):
t2 := NewTree(OrderPeople)
t2.Add(Person{"Bob", 30})
t2.Add(Person{"Maria", 35})
t2.Add(Person{"Bob", 50})
fmt.Println(t2.Contains(Person{"Bob", 30}))
fmt.Println(t2.Contains(Person{"Fred", 25}))
不使用函數(shù),我們了可以為??NewTree?
?提供一個(gè)方法。在??方法也是函數(shù)??中我們討論過,可以使用方法表達(dá)式來將方法看作函數(shù)。下面上手操作。首先編寫方法:
func (p Person) Order(other Person) int {out := cmp.Compare(p.Name, other.Name)if out == 0 {out = cmp.Compare(p.Age, other.Age)}return out
}
然后使用該方法:
t3 := NewTree(Person.Order)
t3.Add(Person{"Bob", 30})
t3.Add(Person{"Maria", 35})
t3.Add(Person{"Bob", 50})
fmt.Println(t3.Contains(Person{"Bob", 30}))
fmt.Println(t3.Contains(Person{"Fred", 25}))
可在??The Go Playground???或??第8章的GitHub代碼庫??的sample_code/generic_tree目錄下測(cè)試這段代碼。
再談可比較類型
在??接口可比較??一節(jié)中我們學(xué)到,接口中也是Go中一種可比較類型。這也就表示在對(duì)接口類型變量使用??==?
?和??!=?
?時(shí)要小心。如果接口的底層類型不可比較,代碼會(huì)在運(yùn)行時(shí)panic。
這個(gè)坑在使用帶泛型的可比較接口時(shí)依然存在。假設(shè)我們定義了一個(gè)接口以及一些實(shí)現(xiàn):
type Thinger interface {Thing()
}type ThingerInt intfunc (t ThingerInt) Thing() {fmt.Println("ThingInt:", t)
}type ThingerSlice []intfunc (t ThingerSlice) Thing() {fmt.Println("ThingSlice:", t)
}
還需要定義一個(gè)泛型函數(shù)僅接收可比較的值:
func Comparer[T comparable](t1, t2 T) {if t1 == t2 {fmt.Println("equal!")}
}
調(diào)用帶類型為??int?
?或??ThingerInt?
?的變量的函數(shù)完全合法:
var a int = 10
var b int = 10
Comparer(a, b) // prints truevar a2 ThingerInt = 20
var b2 ThingerInt = 20
Comparer(a2, b2) // prints true
編譯器不允許我們調(diào)用變量類型為??ThingerSlice?
?(或??[]int?
?)的函數(shù):
var a3 ThingerSlice = []int{1, 2, 3}
var b3 ThingerSlice = []int{1, 2, 3}
Comparer(a3, b3) // compile fails: "ThingerSlice does not satisfy comparable"
但所調(diào)用的變量類型為??Thinger?
?時(shí)完全合法。如果使用??ThingerInt?
?,代碼可正常編譯、運(yùn)行:
var a4 Thinger = a2
var b4 Thinger = b2
Comparer(a4, b4) // prints true
但也可以將??ThingerSlice?
?賦值給??Thinger?
?類型的變量。這時(shí)會(huì)出問題:
a4 = a3
b4 = b3
Comparer(a4, b4) // compiles, panics at runtime
編譯器允許我們構(gòu)建這段代碼,但運(yùn)行后程序會(huì)panic(參見??panic和recover??一節(jié)了解更多信息),消息為??panic: runtime error: comparing uncomparable type main.ThingerSlice?
???稍??The Go Playground???或??第8章的GitHub代碼庫??的sample_code/more_comparable目錄下測(cè)試這段代碼。
在關(guān)可比較類型和泛型交互以及為何做出這種設(shè)計(jì)決策的更多技術(shù)細(xì)節(jié),請(qǐng)閱讀Go團(tuán)隊(duì)Robert Griesemer的博客文章??All your comparable types??。
未實(shí)現(xiàn)的功能
Go仍是一種小型且聚焦的編程語言,Go對(duì)泛型的實(shí)現(xiàn)并未包含部分在其它語言泛型中存在的特性。下面是一些Go泛型尚未實(shí)現(xiàn)的特性。
雖然我們可以構(gòu)建一個(gè)同時(shí)能處理自定義和內(nèi)置類型的樹,但在Python、Ruby和C++中處理的方法卻不同。它們有運(yùn)算符重載,允許用戶自定義類型指定運(yùn)算符的實(shí)現(xiàn)。 Go沒有添加這種特性。也就意味著我們不能使用??range?
?遍歷自定義容器類型,也不能對(duì)其使用??[]?
?進(jìn)行索引。
沒添加運(yùn)算符重載有一些原因。其一是Go語言中有極其大量的運(yùn)算符。Go也不支持函數(shù)或方法重載,那就需要為不同的類型指定不同的運(yùn)算函數(shù)。此外,重載的代碼會(huì)不易理解,因?yàn)殚_發(fā)人員會(huì)為符號(hào)巧立各種含義(在C++中,??<<?
?對(duì)一些類型表示按位左移,而對(duì)另一些類型則在左側(cè)值的右側(cè)寫值)。Go努力避免這類易讀性問題。
另一個(gè)未實(shí)現(xiàn)的有用特性是,Go的泛型實(shí)現(xiàn)對(duì)方法沒有附加類型參數(shù)?;乜??Map/Reduce/Filter?
?函數(shù),你可能覺得它們可像方法那樣使用,如:
type functionalSlice[T any] []T// THIS DOES NOT WORK
func (fs functionalSlice[T]) Map[E any](f func(T) E) functionalSlice[E] {out := make(functionalSlice[E], len(fs))for i, v := range fs {out[i] = f(v)}return out
}// THIS DOES NOT WORK
func (fs functionalSlice[T]) Reduce[E any](start E, f func(E, T) E) E {out := startfor _, v := range fs {out = f(out, v)}return out
}
你以為可以這樣用:
var numStrings = functionalSlice[string]{"1", "2", "3"}
sum := numStrings.Map(func(s string) int {v, _ := strconv.Atoi(s)return v
}).Reduce(0, func(acc int, cur int) int {return acc + cur
})
可惜對(duì)于函數(shù)式編程的擁躉們,并不能這樣用。我們不能做鏈?zhǔn)椒椒ㄕ{(diào)用,而要嵌套函數(shù)調(diào)用或使用更易讀一次調(diào)用一次函數(shù)的方式,將中間值賦給變量。類型參數(shù)提案中詳細(xì)討論了未支持參數(shù)化方法的原因。
沒有可變類型參數(shù)。在??可變參數(shù)和切片??一節(jié)中討論到,要實(shí)現(xiàn)接收可變數(shù)量參數(shù)的函數(shù),需要指定最后一個(gè)參數(shù),其類型以??...?
?開頭。比如,無法對(duì)可變參數(shù)指定某種類型模式,像可交替的??string?
?和??int?
?。所有的可變變量必須為同一種聲明類型,是不是泛型皆可。
Go泛型未實(shí)現(xiàn)的其它特性就更加晦澀些了。有:
特化(Specialization)函數(shù)或方法可通過泛型版本外的一個(gè)或多個(gè)指定類型版本進(jìn)行重載。因Go語言沒有重載,這一特性不在考慮范圍內(nèi)??吕锘?#xff08;Currying)允許我們通過指定某些類型參數(shù)根據(jù)另一個(gè)泛型函數(shù)或類型部分實(shí)例化函數(shù)。元編程允許我們指定在編譯時(shí)運(yùn)行的代碼并生成運(yùn)行時(shí)運(yùn)行的代碼。
地道的Go和泛型
添加泛型顯然會(huì)改變一些地道使用Go的建議。使用??float64?
?來表示所有的數(shù)值類型的時(shí)代結(jié)束了。應(yīng)當(dāng)使用??any?
?來代替??interface{}?
?表示數(shù)據(jù)結(jié)構(gòu)或函數(shù)參數(shù)中未指定的類型??梢杂靡粋€(gè)點(diǎn)函數(shù)處理不同的切片類型。但不要覺得要馬上使用類型參數(shù)切換掉所有的代碼。在新設(shè)計(jì)模式發(fā)明和深化的同時(shí)老代碼依然正??捎谩?/p>
現(xiàn)在判斷泛型對(duì)性能的長(zhǎng)期影響還為時(shí)尚早。在寫本文時(shí),它對(duì)編譯時(shí)間并沒有影響。Go 1.18的編譯器要慢于之前的版本,但Go 1.20的編譯器解決了這一問題。
有一些關(guān)于泛型對(duì)運(yùn)行時(shí)間影響的影響。Vicent Marti寫了一篇??深入的文章??,探討了一些導(dǎo)致代碼變慢的泛型案例并詳細(xì)講解了產(chǎn)生這一問題的實(shí)現(xiàn)細(xì)節(jié)。相反,Eli Bendersky寫了一篇??博客文章??說明泛型讓排序算法變快了。
一般來說,不要期望將帶接口參數(shù)的函數(shù)修改為泛型類型參數(shù)的函數(shù)能提升性能。比如,將下面的小函數(shù):
type Ager interface {age() int
}func doubleAge(a Ager) int {return a.age() * 2
}
轉(zhuǎn)化為:
func doubleAgeGeneric[T Ager](a T) int {return a.age() * 2
}
會(huì)使得該函數(shù)在Go 1.20變慢約30%。(對(duì)于大型函數(shù),沒有顯著的性能區(qū)別)??梢允褂??第8章的GitHub代碼庫??的sample_code/perf directory目錄下代碼進(jìn)行基準(zhǔn)測(cè)試。
使用過其它語言泛型的開發(fā)者可能會(huì)感到意外。比如在C++中,編譯器使用抽象數(shù)據(jù)類型的泛型來將運(yùn)行時(shí)運(yùn)算(確定所使用的實(shí)體類型)轉(zhuǎn)化為編譯時(shí)運(yùn)算,為每種實(shí)體類型生成獨(dú)立的函數(shù)。這會(huì)讓二進(jìn)制變大,但也讓其變快。Vicent在博客文章中提到,當(dāng)前的Go編譯器僅為不同的底層類型生成獨(dú)立函數(shù)。此外,所有指針類型共享同一個(gè)生成函數(shù)。為區(qū)分傳遞給共享生成函數(shù)的類型,編譯器添加了額外的運(yùn)行時(shí)查詢。這會(huì)減慢性能。
隨著Go未來版本中泛型實(shí)現(xiàn)漸趨成熟,運(yùn)行時(shí)性能也會(huì)提升。目標(biāo)并沒有改變,還是要編寫滿足需求且易維護(hù)的快速運(yùn)行代碼。使用??基準(zhǔn)測(cè)試??一節(jié)中討論的基準(zhǔn)測(cè)試和性能測(cè)試工具來度量和提升你的代碼。
向標(biāo)準(zhǔn)庫添加泛型
Go 1.18剛發(fā)布泛型時(shí)是很保守的。在全局添加了??any?
?和??comparable?
?接口,但并未在標(biāo)準(zhǔn)庫中做出支持泛型的API調(diào)整。只做出了樣式變化,將大部分標(biāo)準(zhǔn)庫中的??interface{}?
?改成了??any?
?。
現(xiàn)在Go社區(qū)更適應(yīng)了泛型,我們也看到了更多的變化。從Go 1.21起,標(biāo)準(zhǔn)庫中包含了一些函數(shù),使用泛型實(shí)現(xiàn)切片、字典和并發(fā)的常用算法。在??復(fù)合類型??一文中我們講到了??slices?
?和??maps?
?包中的??Equal?
?和??EqualFunc?
?函數(shù)。這些包中的其它函數(shù)簡(jiǎn)化了切片和字典操作。??slices?
?包中的??Insert?
?、??Delete?
?和??DeleteFunc?
? 函數(shù)讓開發(fā)展不必構(gòu)建極其復(fù)雜的切片處理代碼。??maps.Clone?
?函數(shù)利用Go Runtime來提供更快速的方式,來創(chuàng)建字典的淺拷貝。在??代碼精確地只運(yùn)行一次??一節(jié)中,我們學(xué)到??sync.OnceValue?
?和??sync.OnceValues?
?,它們使用泛型來構(gòu)建只運(yùn)行一次并返回一到兩個(gè)值的函數(shù)。推薦使用這些包中的函數(shù),而不要自己去實(shí)現(xiàn)。未來版本的標(biāo)準(zhǔn)庫還會(huì)包含更多用到泛型的函數(shù)和類型。
解鎖未來特性
泛型可能是其它未來特性的基礎(chǔ)。一個(gè)可能是sum types。就像類型元素用于指定可替換類型參數(shù)的類型一樣,和類型可用于變量參數(shù)中的接口。這會(huì)出現(xiàn)一些有趣的特性。如今Go在JSON的常見場(chǎng)景存在問題:其字段可以是單個(gè)值也可是值列表。即使是有泛型,處理這種情況的唯一方式是裝飾字段類型設(shè)為??any?
?。添加和類型可讓我們創(chuàng)建指定字段可為字符串、字符串切片及其它類型的接口。然后類型switch可以枚舉每種有效類型,提升類型案例。指定類型邊界集的能力可以讓現(xiàn)代語言(包括Rust和Swift)使用和類型替代枚舉。而Go當(dāng)前在枚舉特性上存在不足,這會(huì)成為一種有吸引力的解決方案,但需要時(shí)間來評(píng)估和探討這些想法 。
小結(jié)
本文中我們學(xué)習(xí)了泛型以及如何使用泛型來簡(jiǎn)化代碼。對(duì)于Go來說泛型還處于早期除非。有它伴隨Go語言不忘初心的成長(zhǎng)還是很讓人激動(dòng)的。
本文來自正在規(guī)劃的?Go語言&云原生自我提升系列??,歡迎關(guān)注后續(xù)文章。