Go 泛型定了,有哪些好的使用場景,哪些不好的應用場景,亦或哪些使用看起來丑?本文聊聊這個問題。
01 簡介
泛型很棒,而且 Go 變得比以前更方便了。但是與可能非常有用的 channel 類似,我們不應該僅僅因為它們存在就到處使用它們。
除了用于數(shù)據(jù)結構,泛型還有其他很好的應用場景。當然,也有一些不好的用例,比如泛型日志器。還有一些可以使用的解決方案,但相當丑陋,還有一些東西真的很丑。
讓我們分別看一個例子!
02 好的應用場景
我真正夢想在 Go 中做的以及我認為我現(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)它。它可以簡單地提供非常強大的具體類型(但仍然可以為簡單的用例抽象)
- 我們可以擴展這個解決方案并使用具體類型進行操作。讓我們看看插入或更新的驗證器會是什么樣子。
type HTTPHandler[MODEL any] struct {
dataProvider DataProvider[MODEL]
InsertValidator func(new MODEL) error
UpdateValidator func(old MODEL, new MODEL) error
}
在這個驗證器中是泛型方法的真正優(yōu)勢所在。我們將解析 HTTP 請求,如果定義了自定義的 InsertValidator,那么我們可以使用它來驗證模型是否檢出,我們可以以類型安全的方式進行并使用具體模型:
type User struct {
FirstName string
LastName string
}
func InsertValidator(u User) error {
if u.FirstName == "" { ... }
if u.LastName == "" { ... }
}
所以我們有一個泛型的處理器,我們可以用自定義回調來調整它,它直接為我們獲取有效負載。沒有類型轉換。沒有 map。只有結構體本身!
03 不好的應用場景
一起看一個泛型日志器的例子:
type GenericLogger[T any] interface {
WithField(string, string) T
Info(string)
}
這本身還不是很有用。有更簡單的方法可以將鍵值字符串對添加到日志器,并且沒有日志器(據(jù)我所知)實際實現(xiàn)此接口。我們也不需要新的日志標準。如果我們想使用 logrus[1],我們必須這樣做:
type GenericLogger[T any, FIELD map[string]interface{}] interface{
WithFields(M) T
Info(string)
}
如果我們添加自引用部分,這實際上可能由 logrus 日志器實現(xiàn)。但是,讓我們考慮在實際結構體中使用它,例如某種處理程序:
type MessageHandler[T GenericLogger[T], FIELD map[string]interface{}] struct {
logger GenericLogger[T, FIELD]
}
為了在結構體中使用這個日志器,我們需要使我們的結構體泛型,這僅適用于日志器。如果 MessageHandler 本身正在處理泛型消息,那將變成第三個類型參數(shù)!
到目前為止,甚至沒有辦法將其分配給具有泛型的變量。所以,盡管我們可以用一個接口來表示這個日志器很棒,但我實際上建議不要這樣做。而我最喜歡的日志庫 (zap[2]),由于其字段的性質,甚至無法用它來表示。
04 丑的場景
當我使用泛型時,我發(fā)現(xiàn)缺少對在方法中引入新泛型參數(shù)的支持。雖然這可能有很好的理由,但它確實需要一些解決方法。讓我們想象一下我們想要將一個 map 簡化為一個整數(shù)。理想情況下,我們將通過使用返回新泛型參數(shù)的方法來完成此操作,然后我們可以簡單地提供 map reduce 函數(shù)。
那么,當我們仍然想以泛型方式縮小該 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 的一部分,那就太好了。即使不是,我們仍然可以模仿這種行為。總的來說,我可能仍會在某些用例中使用這種模式,即使它實際上很丑陋。
05 真的很丑
有時你可能想要使用工廠模式,它為你提供諸如 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
}
雖然這有效,雖然它可能有用,但它是很丑。它將丑陋(反射)與更丑陋(泛型)的東西結合在一起。
雖然從技術上講這應該是類型安全的,但由于我們的復合鍵具有名稱和反射類型,它仍然很難看。我是否要把它放在生產代碼的任何地方,我會很糾結。
06 總結
雖然我喜歡泛型,但我認為很難取得平衡,尤其是在開始的時候。所以我們需要確保記住它們?yōu)槭裁创嬖冢谑裁辞闆r下我們應該使用它們,什么時候我們應該避免它們!
原文鏈接: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