Go1.18 泛型的好、壞亦或丑?
Go 泛型定了,有哪些好的使用場(chǎng)景,哪些不好的應(yīng)用場(chǎng)景,亦或哪些使用看起來(lái)丑?本文聊聊這個(gè)問(wèn)題。
01 簡(jiǎn)介
泛型很棒,而且 Go 變得比以前更方便了。但是與可能非常有用的 channel 類似,我們不應(yīng)該僅僅因?yàn)樗鼈兇嬖诰偷教幨褂盟鼈儭?/p>
除了用于數(shù)據(jù)結(jié)構(gòu),泛型還有其他很好的應(yīng)用場(chǎng)景。當(dāng)然,也有一些不好的用例,比如泛型日志器。還有一些可以使用的解決方案,但相當(dāng)丑陋,還有一些東西真的很丑。
讓我們分別看一個(gè)例子!
02 好的應(yīng)用場(chǎng)景
我真正夢(mèng)想在 Go 中做的以及我認(rèn)為我現(xiàn)在終于可以做的是 CRUD 端點(diǎn)的泛型提供程序:
type?Model?interface?{
????ID()?string
}
type?DataProvider[MODEL?Model]?interface?{
????FindByID(id?string)?(MODEL,?error)
????List()?([]MODEL,?error)
????Update(id?string,?model?MODEL)?error
????Insert(model?MODEL)?error
????Delete(id?string)?error
}
這是一個(gè)大接口,你可以根據(jù)具體用例的需要縮短它,但是,為了完整性起見(jiàn),我們暫時(shí)就這么寫。
現(xiàn)在你可以定義一個(gè)使用 DataProvider 的 HTTP 處理程序:
type?HTTPHandler[MODEL?Model]?struct?{
????dataProvider?DataProvider[MODEL]
}
func?(h?HTTPHandler[MODEL])?FindByID(rw?http.ResponseWriter,?req?*http.Request)?{
????//?validate?request?here
????id?=?//?extract?id?here
????model,?err?:=?h.dataProvider.FindByID(id)
????if?err?!=?nil?{
????????//?error?handling?here
????????return
????}
????err?=?json.NewEncoder(rw).Encode(model)
????if?err?!=?nil?{
????????//?error?handling?here
????????return
????}
}
如你所見(jiàn),我們可以為每個(gè)方法實(shí)現(xiàn)一次,然后我們就完成了。我們甚至可以在事物的另一端創(chuàng)建一個(gè)客戶端,我們只需要為基本方法實(shí)現(xiàn)一次。
為什么我們?cè)谶@里使用泛型而不是簡(jiǎn)單的我們已經(jīng)定義的 Model 接口?
與在此處使用 Model 類型本身相比,泛型有一些優(yōu)點(diǎn):
使用泛型方法,DataProvider 根本不需要知道 Model,也不需要實(shí)現(xiàn)它。它可以簡(jiǎn)單地提供非常強(qiáng)大的具體類型(但仍然可以為簡(jiǎn)單的用例抽象) 我們可以擴(kuò)展這個(gè)解決方案并使用具體類型進(jìn)行操作。讓我們看看插入或更新的驗(yàn)證器會(huì)是什么樣子。
type?HTTPHandler[MODEL?any]?struct?{
????dataProvider?DataProvider[MODEL]
????InsertValidator?func(new?MODEL)?error
????UpdateValidator?func(old?MODEL,?new?MODEL)?error
}在這個(gè)驗(yàn)證器中是泛型方法的真正優(yōu)勢(shì)所在。我們將解析 HTTP 請(qǐng)求,如果定義了自定義的 InsertValidator,那么我們可以使用它來(lái)驗(yàn)證模型是否檢出,我們可以以類型安全的方式進(jìn)行并使用具體模型:
type?User?struct?{
????FirstName?string
????LastName?string
}
func?InsertValidator(u?User)?error?{
????if?u.FirstName?==?""?{?...?}?
????if?u.LastName?==?""?{?...?}
}
所以我們有一個(gè)泛型的處理器,我們可以用自定義回調(diào)來(lái)調(diào)整它,它直接為我們獲取有效負(fù)載。沒(méi)有類型轉(zhuǎn)換。沒(méi)有 map。只有結(jié)構(gòu)體本身!
03 不好的應(yīng)用場(chǎng)景
一起看一個(gè)泛型日志器的例子:
type?GenericLogger[T?any]?interface?{
????WithField(string,?string)?T
????Info(string)
}
這本身還不是很有用。有更簡(jiǎn)單的方法可以將鍵值字符串對(duì)添加到日志器,并且沒(méi)有日志器(據(jù)我所知)實(shí)際實(shí)現(xiàn)此接口。我們也不需要新的日志標(biāo)準(zhǔn)。如果我們想使用 logrus[1],我們必須這樣做:
type?GenericLogger[T?any,?FIELD?map[string]interface{}]?interface{
????WithFields(M)?T
????Info(string)
}
如果我們添加自引用部分,這實(shí)際上可能由 logrus 日志器實(shí)現(xiàn)。但是,讓我們考慮在實(shí)際結(jié)構(gòu)體中使用它,例如某種處理程序:
type?MessageHandler[T?GenericLogger[T],?FIELD?map[string]interface{}]?struct?{
????logger?GenericLogger[T,?FIELD]
}
為了在結(jié)構(gòu)體中使用這個(gè)日志器,我們需要使我們的結(jié)構(gòu)體泛型,這僅適用于日志器。如果 MessageHandler 本身正在處理泛型消息,那將變成第三個(gè)類型參數(shù)!
到目前為止,甚至沒(méi)有辦法將其分配給具有泛型的變量。所以,盡管我們可以用一個(gè)接口來(lái)表示這個(gè)日志器很棒,但我實(shí)際上建議不要這樣做。而我最喜歡的日志庫(kù) (zap[2]),由于其字段的性質(zhì),甚至無(wú)法用它來(lái)表示。
04 丑的場(chǎng)景
當(dāng)我使用泛型時(shí),我發(fā)現(xiàn)缺少對(duì)在方法中引入新泛型參數(shù)的支持。雖然這可能有很好的理由,但它確實(shí)需要一些解決方法。讓我們想象一下我們想要將一個(gè) map 簡(jiǎn)化為一個(gè)整數(shù)。理想情況下,我們將通過(guò)使用返回新泛型參數(shù)的方法來(lái)完成此操作,然后我們可以簡(jiǎn)單地提供 map reduce 函數(shù)。
那么,當(dāng)我們?nèi)匀幌胍苑盒头绞娇s小該 map 時(shí),我們?cè)撛趺崔k?既然沒(méi)有方法,那么讓我們創(chuàng)建一個(gè)方法:
type?GenericMap[KEY?comparable,?VALUE?any]?map[KEY]VALUEfunc?(g?GenericMap[KEY,?VALUE])?Values()?[]VALUE?{
????values?:=?make([]VALUE,?len(g))
????for?_,?v?:=?range?g?{
????????values?=?append(values,?v)
????}
????return?values
}
func?Reduce[KEY?comparable,?VALUE?any,?RETURN?any](g?GenericMap[KEY,?VALUE],?callback?func(RETURN,?KEY,?VALUE?"KEY?comparable,?VALUE?any,?RETURN?any")?RETURN)?RETURN?{
????var?r?RETURN
????for?k,?v?:=?range?g?{
????????r?=?callback(r,?k,?v)
????}
????return?r
}
GenericMap 成為第一個(gè)參數(shù)或我們的 Reduce 函數(shù)。在這種情況下,你可以使用任何類型的 map 作為第一個(gè)參數(shù),而不是 GenericMap。然而,我想說(shuō)明的一點(diǎn)是,如果這個(gè)方法本身是 GenericMap 的一部分,那就太好了。即使不是,我們?nèi)匀豢梢阅7逻@種行為。總的來(lái)說(shuō),我可能仍會(huì)在某些用例中使用這種模式,即使它實(shí)際上很丑陋。
05 真的很丑
有時(shí)你可能想要使用工廠模式,它為你提供諸如 DataProviders 之類的東西。你可能希望在動(dòng)態(tài)注冊(cè)的端點(diǎn)上獲取提供程序。所以你可以這樣做:
type?DataProviderFactory?struct?{
????dataProviders?map[providerKey]any
}func?ProviderByName[MODEL?Model](factory?*DataProviderFactory,?name?string?"MODEL?Model")?(DataProvider[MODEL],?bool)?{
????????var?m?MODEL
????prov,?has?:=?factory.dataProviders[providerKey{name:?name,?typ:?reflect.TypeOf(m)}]
????if?!has?{
???????return?nil,?false
????}
????return?prov.(DataProvider[MODEL]),?true?
}func?RegisterProvider[MODEL?Model](factory?*DataProviderFactory,?name?string,?p?DataProvider[MODEL]?"MODEL?Model")?{
????var?m?MODEL
????factory.dataProviders[providerKey{name:?name,?typ:?reflect.TypeOf(m)}]?=?p?
}
雖然這有效,雖然它可能有用,但它是很丑。它將丑陋(反射)與更丑陋(泛型)的東西結(jié)合在一起。
雖然從技術(shù)上講這應(yīng)該是類型安全的,但由于我們的復(fù)合鍵具有名稱和反射類型,它仍然很難看。我是否要把它放在生產(chǎn)代碼的任何地方,我會(huì)很糾結(jié)。
06 總結(jié)
雖然我喜歡泛型,但我認(rèn)為很難取得平衡,尤其是在開(kāi)始的時(shí)候。所以我們需要確保記住它們?yōu)槭裁创嬖冢谑裁辞闆r下我們應(yīng)該使用它們,什么時(shí)候我們應(yīng)該避免它們!
原文鏈接:https://itnext.io/golang-1-18-generics-the-good-the-bad-the-ugly-5e9fa2520e76
參考資料
logrus: https://github.com/sirupsen/logrus
[2]zap: https://github.com/uber-go/zap
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語(yǔ)言并創(chuàng)建了 Go 語(yǔ)言中文網(wǎng)!著有《Go語(yǔ)言編程之旅》、開(kāi)源圖書《Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)》等。
堅(jiān)持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場(chǎng)心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長(zhǎng)!也歡迎加我微信好友交流:gopherstudio
