欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文教你如何在Golang中用好泛型

 更新時間:2023年07月24日 15:12:16   作者:apocelipes  
golang的泛型已經(jīng)出來了一年多了,從提案被接受開始我就在關(guān)注泛型了,好用是好用,但問題也很多,所以本文就來教大家如何在Golang中用好泛型吧

golang的泛型已經(jīng)出來了一年多了,從提案被接受開始我就在關(guān)注泛型了,如今不管是在生產(chǎn)環(huán)境還是開源項目里我都寫了不少泛型代碼,是時候全面得回顧下golang泛型的使用體驗了。

先說說結(jié)論,好用是好用,但問題也很多,有些問題比較影響使用體驗,到了不吐不快的地步了。

這篇文章不會教你泛型的基礎(chǔ)語法,并且要求你對golang的泛型使用有一定經(jīng)驗,如果你還是個泛型的新手,可以先閱讀下官方的教程,然后再閱讀本篇文章。

泛型的實現(xiàn)

實現(xiàn)泛型有很多種方法,常見的主流的是下面這些:

  • 以c++為代表的,類型參數(shù)就是個占位符,最后實際上會替換成實際類型,然后以此為模板生成實際的代碼,生成多份代碼,每份的類型都不一樣
  • 以TypeScript和Java為代表的類型擦除,把類型參數(shù)泛化成一個滿足類型約束的類型(Object或者某個interface),只生成一份代碼
  • 以c#為代表,代碼里表現(xiàn)的像類型擦除,但運行的時候?qū)嶋H上和c++一樣采用模板實例化對每個不同的類型都生成一份代碼

那么golang用的哪種呢?哪種都不是,golang有自己的想法:gcshape。

什么是gcshape?簡單得說,所有擁有相同undelyring type的類型都算同一種shape,所有的指針都算一種shape,除此之外就算兩個類型大小相同甚至字段的類型相同也不算同一個shape

那么這個shape又是什么呢?gc編譯器會根據(jù)每個shape生成一份代碼,擁有相同shape的類型會共用同一份代碼。

看個簡單例子:

func Output[T any]() {
	var t T
	fmt.Printf("%#v\n", t)
}
type A struct {
	a,b,c,d,e,f,g int64
	h,i,j string
	k []string
	l, m, n map[string]uint64
}
type B A
func main() {
	Output[string]()
	Output[int]()
	Output[uint]()
	Output[int64]()
	Output[uint64]() // 上面每個都underlying type都不同,盡管int64和uint64大小一樣,所以生成5份不同的代碼
	Output[*string]()
	Output[*int]()
	Output[*uint]()
	Output[*A]() // 所有指針都是同一個shape,所以共用一份代碼
	Output[A]()
	Output[*B]()
	Output[B]() // B的underlying tyoe和A一樣,所以和A共用代碼
	Output[[]int]()
	Output[*[]int]()
	Output[map[int]string]()
	Output[*map[int]string]()
	Output[chan map[int]string]()
}

驗證也很簡單,看看符號表即可:

為啥要這么做?按提案的說法,這么做是為了避免代碼膨脹同時減輕gc的負(fù)擔(dān),看著是有那么點道理,有相同shape的內(nèi)存布局是一樣的,gc處理起來也更簡單,生成的代碼也確實減少了——如果我就是不用指針那生成的代碼其實也沒少多少。

盡管官方拿不出證據(jù)證明gcshape有什么性能優(yōu)勢,我們還是姑且認(rèn)可它的動機吧。但這么實現(xiàn)泛型后導(dǎo)致了很多嚴(yán)重的問題:

  • 性能不升反降
  • 正常來說類型參數(shù)是可以當(dāng)成普通的類型來用的,但golang里有很多時候不能

正因為有了gcshape,想在golang里用對泛型還挺難的。

性能問題

這一節(jié)先說說性能??磦€例子:

type A struct {
	num  uint64
	num1 int64
}
func (a *A) Add() {
	a.num++
	a.num1 = int64(a.num / 2)
}
type B struct {
	num1 uint64
	num2 int64
}
func (b *B) Add() {
	b.num1++
	b.num2 = int64(b.num1 / 2)
}
type Adder interface {
	Add()
}
func DoAdd[T Adder](t T) {
	t.Add()
}
func DoAddNoGeneric(a Adder) {
	a.Add()
}
func BenchmarkNoGenericA(b *testing.B) {
	obj := &A{}
	for i := 0; i < b.N; i++ {
		obj.Add()
	}
}
func BenchmarkNoGenericB(b *testing.B) {
	obj := &B{}
	for i := 0; i < b.N; i++ {
		obj.Add()
	}
}
func BenchmarkGenericA(b *testing.B) {
	obj := &A{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}
func BenchmarkGenericB(b *testing.B) {
	obj := &B{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}
func BenchmarkGenericInterfaceA(b *testing.B) {
	var obj Adder = &A{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}
func BenchmarkGenericInterfaceB(b *testing.B) {
	var obj Adder = &B{}
	for i := 0; i < b.N; i++ {
		DoAdd(obj)
	}
}
func BenchmarkDoAddNoGeneric(b *testing.B) {
	var obj Adder = &A{}
	for i := 0; i < b.N; i++ {
		DoAddNoGeneric(obj)
	}
}

猜猜結(jié)果,是不是覺得引入了泛型可以解決很多性能問題?答案揭曉:

哈哈,純泛型和正常代碼比有不到10%的差異,而接口+泛型就慢了接近100%。直接用接口是這里最快的,不過這是因為接口被編譯器優(yōu)化了,原因參考這篇。

你說誰會這么寫代碼啊,沒事,我再舉個更常見的例子:

func Search[T Equaler[T]](slice []T, target T) int {
	index := -1
	for i := range slice {
		if slice[i].Equal(target) {
			index = i
		}
	}
	return index
}
type MyInt int
func (m MyInt) Equal(rhs MyInt) bool {
	return int(m) == int(rhs)
}
type Equaler[T any] interface {
	Equal(T) bool
}
func SearchMyInt(slice []MyInt, target MyInt) int {
	index := -1
	for i := range slice {
		if slice[i].Equal(target) {
			index = i
		}
	}
	return index
}
func SearchInterface(slice []Equaler[MyInt], target MyInt) int {
	index := -1
	for i := range slice {
		if slice[i].Equal(target) {
			index = i
		}
	}
	return index
}
var slice []MyInt
var interfaces []Equaler[MyInt]
func init() {
	slice = make([]MyInt, 100)
	interfaces = make([]Equaler[MyInt], 100)
	for i := 0; i < 100; i++ {
		slice[i] = MyInt(i*i + 1)
		interfaces[i] = slice[i]
	}
}
func BenchmarkSearch(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Search(slice, 99*99)
	}
}
func BenchmarkInterface(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SearchInterface(interfaces, 99*99)
	}
}
func BenchmarkSearchInt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		SearchMyInt(slice, 99*99)
	}
}

這是結(jié)果:

泛型代碼和使用接口的代碼相差無幾,比普通代碼慢了整整六倍!

為啥?因為gcshape的實現(xiàn)方式導(dǎo)致了類型參數(shù)T并不是真正的類型,所以在調(diào)用上面的方法的時候得查找一個叫type dict的東西找到當(dāng)前使用的真正的類型,然后再把綁定在T上的變量轉(zhuǎn)換成那個類型。多了一次查找+轉(zhuǎn)換,這里的MyInt轉(zhuǎn)換后還會被復(fù)制一次,所以能不慢么。

這也解釋了為什么把接口傳遞給類型參數(shù)是最慢的,因為除了要查一次type dict,接口本身還得再做一次類型檢查并查找對應(yīng)的method。

所以想靠泛型大幅提升性能的人還是洗洗睡吧,只有一種情況泛型的性能不會更差:在類型參數(shù)上只使用內(nèi)置的運算符比如加減乘除,不調(diào)用任何方法。

但也不該因噎廢食,首先泛型struct和泛型interface受到的影響很小,其次如我所說,如果不使用類型約束上的方法,那性能損耗幾乎沒有,所以像lo、mo這樣的工具庫還是能放心用的。

這個問題1.18就有人提出來了,然而gcshape的實現(xiàn)在這點上太拉胯,小修小補解決不了問題,官方也沒改進(jìn)的動力,所以哪怕到了1.21還是能復(fù)現(xiàn)同樣的問題。

不過噩夢才剛剛開始,更勁爆的還在后面呢。

如何創(chuàng)建對象

首先你不能這么寫:T{},因為int之類的內(nèi)置類型不支持這么做。也不能這樣:make(T, 0),因為T不是類型占位符,不知道具體類型是什么,萬一是不能用make的類型編譯會報錯。

那么對于一個類型T,想要在泛型函數(shù)里創(chuàng)建一個它的實例就只能這樣了:

func F[T any]() T {
    var ret T
    // 如果需要指針,可以用new(T),但有注意事項,下面會說
    return ret
}

So far, so good。那么我要把T的類型約束換成一個有方法的interface呢?

type A struct {i int}
func (*A)Hello() {
	fmt.Println("Hello from A!")
}
func (a *A) Set(i int) {
	a.i = i
}
type B struct{i int}
func (*B)Hello(){
	fmt.Println("Hello from B!")
}
func (b *B) Set(i int) {
	b.i = i
}
type API interface {
	Hello()
	Set(int)
}
func SayHello[PT API](a PT) {
	a.Hello()
	var b PT
	b.Hello()
	b.Set(222222)
	fmt.Println(a, b)
}
func main() {
	a := new(A)
	a.Set(111)
	fmt.Println(a)
	SayHello(&A{})
	SayHello(&B{})
}

運行結(jié)果是啥?啥都不是,運行時會獎勵你一個大大的panic:

你懵了,如果T的約束是any的時候就是好的,雖然不能調(diào)用方法,怎么到這調(diào)Set就空指針錯誤了呢?

這就是我要說的第二點嚴(yán)重問題了,類型參數(shù)不是你期待的那種int,MyInt那種類型,類型參數(shù)有自己獨有的類型,叫type parameter。有興趣可以去看語言規(guī)范里的定義,沒興趣就這么簡單粗暴的理解也夠了:這就是種會編譯期間進(jìn)行檢查的interface。

理解了這點你的問題就迎刃而解了,因為它類似下面的代碼:

var a API
a.Set(1)

a沒綁定任何東西,那么調(diào)Set百分百空指針錯誤。同理,SayHello里的b也沒綁定任何數(shù)據(jù),一樣會空指針錯誤。為什么b.Hello()調(diào)成功了,因為這個方法里沒對接收器的指針解引用。

同樣new(T)這個時候是創(chuàng)建了一個type parameter的指針,和原類型的關(guān)系就更遠(yuǎn)了。

但對于像這樣~int[]int的有明確的core type的約束,編譯器又是雙標(biāo)的,可以正常創(chuàng)建實例變量。

怎么解決?沒法解決,當(dāng)然不排除是我不會用golang的泛型,如果你知道在不使用unsafe或者給T添加創(chuàng)建實例的新方法的前提下滿足需求的解法,歡迎告訴我。

目前為止這還不是大問題,一般不需要在泛型代碼里創(chuàng)建實例,大部分需要的情況也可以在函數(shù)外創(chuàng)建后傳入。而且golang本身沒有構(gòu)造函數(shù)的概念,怎么創(chuàng)建類型的實例并不是類型的一部分,這點上不支持還是可以理解的。

但下面這個問題就很難找到合理的借口了。

把指針傳遞給類型參數(shù)

最佳實踐:永遠(yuǎn)不要把指針類型作為類型參數(shù),就像永遠(yuǎn)不要獲取interface的指針一樣。

為啥,看看下面的例子就行:

func Set[T *int|*uint](ptr T) {
	*ptr = 1
}
func main() {
	i := 0
	j := uint(0)
	Set(&i)
	Set(&j)
	fmt.Println(i, j)
}

輸出是啥,是編譯錯誤:

$ go build a.go
 
# command-line-arguments
./a.go:6:3: invalid operation: pointers of ptr (variable of type T constrained by *int | *uint) must have identical base types

這個意思是T不是指針類型,沒法解引用。猜都不用猜,肯定又是type parameter作怪了。

是的。T是type parameter,而type parameter不是指針,不支持解引用操作。

不過比起前一個問題,這個是有解決辦法的,而且辦法很多,第一種,明確表明ptr是個指針:

func Set[T int|uint](ptr *T) {
	*ptr = 1
}

第二種,投機取巧:

func Set[T int|uint, PT interface{*T}](ptr PT) {
	*ptr = 1
}

第二種為什么行,因為在類型約束里如果T的約束有具體的core type(包括any),那么在這里就會被當(dāng)成實際的類型用而不是type parameter。所以PT代表的意思是“有一個類型,它必須是T代表的實際類型的指針類型”。因為PT是指針類型了,所以第二種方法也可以達(dá)到目的。

但我永遠(yuǎn)只推薦你用第一種方法,別給自己找麻煩

泛型和類型的方法集

先看一段代碼:

type A struct {i int}
func (*A)Hello() {
	fmt.Println("Hello from A!")
}
type B struct{i int}
func (*B)Hello(){
	fmt.Println("Hello from B!")
}
func SayHello[T ~*A|~*B](a T) {
	a.Hello()
}
func main() {
	SayHello(&A{})
	SayHello(&B{})
}

輸出是啥?又是編譯錯誤:

$ go build a.go
 
# command-line-arguments
./a.go:17:4: a.Hello undefined (type T has no field or method Hello)

你猜到了,因為T是類型參數(shù),而不是(*A),所以沒有對應(yīng)的方法存在。所以你這么改了:

func SayHello[T A|B](a *T) {
	a.Hello()
}

這時候輸出又變了:

$ go build a.go
 
# command-line-arguments
./a.go:17:4: a.Hello undefined (type *T is pointer to type parameter, not type parameter)

這個報錯好像挺眼熟啊,這不就是取了interface的指針之后在指針上調(diào)用方法時報的那個錯嗎?

對,兩個錯誤都差不多,因為type parameter有自己的數(shù)據(jù)結(jié)構(gòu),而它沒有任何方法,所以通過指針指向type parameter后再調(diào)用方法會報一模一樣的錯。

難道我們只能建個interface里面放上Hello這個方法了嗎?雖然我推薦你這么做,但還有別的辦法,我們可以利用上一節(jié)的PT,但需要給它加點method:

func SayHello[T A|B, PT interface{*T; Hello()}](a PT) {
	a.Hello()
}

原理是一樣的,但現(xiàn)在a還同時支持指針的操作。

直接用interface{Hello()}不好嗎?絕大部分時間都可以,但如果我只想限定死某些類型的話就不適用了。

如何復(fù)制一個對象

大部分情況下直接b := a即可,不過要注意這是淺拷貝。

對于指針就比較復(fù)雜了,因為type parameter的存在,我們得特殊處理:

type A struct {i int}
func (*A)Hello() {
	fmt.Println("Hello from A!")
}
func (a *A) Set(i int) {
	a.i = i
}
type B struct{i int/*j*/}
func (*B)Hello(){
	fmt.Println("Hello from B!")
}
func (b *B) Set(i int) {
	b.i = i
}
type API[T any] interface {
	*T
	Set(int)
}
func DoCopy[T any, PT API[T]](a PT) {
	b := *a
	(PT(&b)).Set(222222) // 依舊是淺拷貝
	fmt.Println(a, b)
}

PT是指針類型,所以可以解引用得到T的值,然后再賦值給b,完成了一次淺拷貝。

注意,拷貝出來的b是T類型的,得先轉(zhuǎn)成*T再轉(zhuǎn)成PT。

想深拷貝怎么辦,那只能定義和實現(xiàn)這樣的接口了:CloneAble[T any] interface{Clone() T}。這倒也沒那么不合理,為了避免淺拷貝問題一般也需要提供一個可以復(fù)制自身的方法,算是順勢而為吧。

總結(jié)

這一年多來我遇到的令人不爽的問題就是這些,其中大部分是和指針相關(guān)的,偶爾還要外加一個性能問題。

一些最佳實踐:

  • 明確使用*T,而不是讓T代表指針類型
  • 明確使用[]Tmap[T1]T2,而不是讓T代表slice或map
  • 少寫泛型函數(shù),可以多用泛型struct
  • 類型約束的core type直接影響被約束的類型可以執(zhí)行哪些操作,要當(dāng)心

如果是c++,那不會有這些問題,因為類型參數(shù)是占位符,會被替換成真實的類型;如果是ts,java也不會有這些問題,因為它們沒有指針的概念;如果是c#,也不會有問題,至少在8.0的時候編譯器不允許構(gòu)造類似T*的東西,如果你這么寫,會有清晰明確的錯誤信息。

而我們的golang呢?雖然不支持,但給的報錯卻是一個代碼一個樣,對golang的類型系統(tǒng)和泛型實現(xiàn)細(xì)節(jié)沒點了解還真不知道該怎么處理呢。

我的建議是,在golang想辦法改進(jìn)這些問題之前,只用別人寫的泛型庫,只用泛型處理slice和map。其他的雜技我們就別玩了,容易摔著。

到此這篇關(guān)于一文教你如何在Golang中用好泛型的文章就介紹到這了,更多相關(guān)Golang泛型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Golang 操作TSV文件的實戰(zhàn)示例

    Golang 操作TSV文件的實戰(zhàn)示例

    本文主要介紹了Golang 操作TSV文件的實戰(zhàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • Golang中大端序和小端序的處理

    Golang中大端序和小端序的處理

    大端序和小端序是描述多字節(jié)數(shù)據(jù)在內(nèi)存中存儲順序的術(shù)語,本文主要介紹了Golang中大端序和小端序的處理,具有一定的參考價值,感興趣的可以了解一下
    2025-02-02
  • 淺談Golang 嵌套 interface 的賦值問題

    淺談Golang 嵌套 interface 的賦值問題

    這篇文章主要介紹了淺談Golang 嵌套 interface 的賦值問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • Go語言中常見的文件操作分享

    Go語言中常見的文件操作分享

    文件操作應(yīng)該是應(yīng)用程序里非常常見的一種操作,無論是哪種應(yīng)用場景,幾乎都離不開文件的基本操作。Go語言中提供了三個不同的包去處理文件,下午就來說說它們的具體使用
    2023-01-01
  • 使用go gin來操作cookie的講解

    使用go gin來操作cookie的講解

    今天小編就為大家分享一篇關(guān)于使用go gin來操作cookie的講解,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧
    2019-04-04
  • Gin框架中的GET和POST表單處理的實現(xiàn)

    Gin框架中的GET和POST表單處理的實現(xiàn)

    Gin框架提供了簡單而強大的機制來處理GET和POST表單提交的數(shù)據(jù),通過c.Query、c.PostForm、c.Bind和c.Request.FormFile等方法,可以輕松地獲取和處理各種表單數(shù)據(jù),感興趣的可以了解一下
    2025-03-03
  • golang使用sync.singleflight解決熱點緩存穿透問題

    golang使用sync.singleflight解決熱點緩存穿透問題

    在go的sync包中,有一個singleflight包,里面有一個?singleflight.go文件,代碼加注釋,一共200行出頭,通過?singleflight可以很容易實現(xiàn)緩存和去重的效果,避免重復(fù)計算,接下來我們就給大家詳細(xì)介紹一下sync.singleflight如何解決熱點緩存穿透問題
    2023-07-07
  • Golang實現(xiàn)文件夾的創(chuàng)建與刪除的方法詳解

    Golang實現(xiàn)文件夾的創(chuàng)建與刪除的方法詳解

    這篇文章主要介紹了如何利用Go語言實現(xiàn)對文件夾的常用操作:創(chuàng)建于刪除。文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-05-05
  • Go?實現(xiàn)?WebSockets和什么是?WebSockets

    Go?實現(xiàn)?WebSockets和什么是?WebSockets

    這篇文章主要介紹了Go?實現(xiàn)?WebSockets和什么是?WebSockets,WebSockets?是構(gòu)建實時應(yīng)用程序的第一大解決方案,在線游戲、即時通訊、跟蹤應(yīng)用程序等,下文相關(guān)內(nèi)容介紹需要的小伙伴可以參考一下
    2022-04-04
  • 詳解Golang官方中的一致性哈希組件

    詳解Golang官方中的一致性哈希組件

    這篇文章主要為大家詳細(xì)介紹了Golang官方中的一致性哈希組件的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-04-04

最新評論