Go1.18 泛型的好、壞亦或丑?
大家好,我是程序員幽鬼。
Go 泛型定了,有哪些好的使用場景,哪些不好的應(yīng)用場景,亦或哪些使用看起來丑?本文聊聊這個問題。
1 簡介
泛型很棒,而且 Go 變得比以前更方便了。但是與可能非常有用的 channel 類似,我們不應(yīng)該僅僅因為它們存在就到處使用它們。
除了用于數(shù)據(jù)結(jié)構(gòu),泛型還有其他很好的應(yīng)用場景。當(dāng)然,也有一些不好的用例,比如泛型日志器。還有一些可以使用的解決方案,但相當(dāng)丑陋,還有一些東西真的很丑。
讓我們分別看一個例子!
2 好的應(yīng)用場景
我真正夢想在 Go 中做的以及我認(rèn)為我現(xiàn)在終于可以做的是 CRUD 端點的泛型提供程序:
- 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
- }
這是一個大接口,你可以根據(jù)具體用例的需要縮短它,但是,為了完整性起見,我們暫時就這么寫。
現(xiàn)在你可以定義一個使用 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
- }
- }
如你所見,我們可以為每個方法實現(xiàn)一次,然后我們就完成了。我們甚至可以在事物的另一端創(chuàng)建一個客戶端,我們只需要為基本方法實現(xiàn)一次。
為什么我們在這里使用泛型而不是簡單的我們已經(jīng)定義的 Model 接口?
與在此處使用 Model 類型本身相比,泛型有一些優(yōu)點:
- 使用泛型方法,DataProvider 根本不需要知道 Model,也不需要實現(xiàn)它。它可以簡單地提供非常強(qiáng)大的具體類型(但仍然可以為簡單的用例抽象)
- 我們可以擴(kuò)展這個解決方案并使用具體類型進(jìn)行操作。讓我們看看插入或更新的驗證器會是什么樣子。
- type HTTPHandler[MODEL any] struct {
- dataProvider DataProvider[MODEL]
- InsertValidator func(new MODEL) error
- UpdateValidator func(old MODEL, new MODEL) error
- }
在這個驗證器中是泛型方法的真正優(yōu)勢所在。我們將解析 HTTP 請求,如果定義了自定義的 InsertValidator,那么我們可以使用它來驗證模型是否檢出,我們可以以類型安全的方式進(jìn)行并使用具體模型:
- type User struct {
- FirstName string
- LastName string
- }
- func InsertValidator(u User) error {
- if u.FirstName == "" { ... }
- if u.LastName == "" { ... }
- }
所以我們有一個泛型的處理器,我們可以用自定義回調(diào)來調(diào)整它,它直接為我們獲取有效負(fù)載。沒有類型轉(zhuǎn)換。沒有 map。只有結(jié)構(gòu)體本身!
3 不好的應(yīng)用場景
一起看一個泛型日志器的例子:
- type GenericLogger[T any] interface {
- WithField(string, string) T
- Info(string)
- }
這本身還不是很有用。有更簡單的方法可以將鍵值字符串對添加到日志器,并且沒有日志器(據(jù)我所知)實際實現(xiàn)此接口。我們也不需要新的日志標(biāo)準(zhǔn)。如果我們想使用 logrus[1],我們必須這樣做:
- type GenericLogger[T any, FIELD map[string]interface{}] interface{
- WithFields(M) T
- Info(string)
- }
如果我們添加自引用部分,這實際上可能由 logrus 日志器實現(xiàn)。但是,讓我們考慮在實際結(jié)構(gòu)體中使用它,例如某種處理程序:
- type MessageHandler[T GenericLogger[T], FIELD map[string]interface{}] struct {
- logger GenericLogger[T, FIELD]
- }
為了在結(jié)構(gòu)體中使用這個日志器,我們需要使我們的結(jié)構(gòu)體泛型,這僅適用于日志器。如果 MessageHandler 本身正在處理泛型消息,那將變成第三個類型參數(shù)!
到目前為止,甚至沒有辦法將其分配給具有泛型的變量。所以,盡管我們可以用一個接口來表示這個日志器很棒,但我實際上建議不要這樣做。而我最喜歡的日志庫 (zap[2]),由于其字段的性質(zhì),甚至無法用它來表示。
4 丑的場景
當(dāng)我使用泛型時,我發(fā)現(xiàn)缺少對在方法中引入新泛型參數(shù)的支持。雖然這可能有很好的理由,但它確實需要一些解決方法。讓我們想象一下我們想要將一個 map 簡化為一個整數(shù)。理想情況下,我們將通過使用返回新泛型參數(shù)的方法來完成此操作,然后我們可以簡單地提供 map reduce 函數(shù)。
那么,當(dāng)我們?nèi)匀幌胍苑盒头绞娇s小該 map 時,我們該怎么辦?既然沒有方法,那么讓我們創(chuàng)建一個方法:
- 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 成為第一個參數(shù)或我們的 Reduce 函數(shù)。在這種情況下,你可以使用任何類型的 map 作為第一個參數(shù),而不是 GenericMap。然而,我想說明的一點是,如果這個方法本身是 GenericMap 的一部分,那就太好了。即使不是,我們?nèi)匀豢梢阅7逻@種行為??偟膩碚f,我可能仍會在某些用例中使用這種模式,即使它實際上很丑陋。
5 真的很丑
有時你可能想要使用工廠模式,它為你提供諸如 DataProviders 之類的東西。你可能希望在動態(tài)注冊的端點上獲取提供程序。所以你可以這樣做:
- 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)代碼的任何地方,我會很糾結(jié)。
6 總結(jié)
雖然我喜歡泛型,但我認(rèn)為很難取得平衡,尤其是在開始的時候。所以我們需要確保記住它們?yōu)槭裁创嬖?,在什么情況下我們應(yīng)該使用它們,什么時候我們應(yīng)該避免它們!
原文鏈接:https://itnext.io/golang-1-18-generics-the-good-the-bad-the-ugly-5e9fa2520e76
參考資料
[1]logrus: https://github.com/sirupsen/logrus
[2]zap: https://github.com/uber-go/zap
本文轉(zhuǎn)載自微信公眾號「幽鬼」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系幽鬼公眾號。