詳解如何利用Golang泛型提高編碼效率
前言
Golang的泛型已經(jīng)出來有一段時間了,大家應(yīng)該或多或少對它有所了解,甚至已經(jīng)在應(yīng)用中使用它。雖然Golang的泛型在功能上確實比較簡單,而且確實可能會增加代碼的復(fù)雜度,過度使用可能還會降低代碼可讀性。
但不可否認泛型確實讓我們在使用Golang的時候能夠抽取一些通用的代碼,避免代碼的重復(fù)拷貝,提高代碼性能(避免類型轉(zhuǎn)換),提高編碼的效率和體驗,提高代碼可維護性。
這篇文章主要是介紹我使用Golang泛型做過的事情。
工具函數(shù)
雖然標準庫里面已經(jīng)提供了大量的工具函數(shù),但是這些工具函數(shù)都沒有使用泛型實現(xiàn),為了提高使用體驗,我們可以使用泛型進行實現(xiàn)。
比如數(shù)值算法里很經(jīng)典的math.Max()
、math.Min()
都是float64
類型的,但是很多時候我們使用的是int
、int64
這些類型,在Golang引入泛型之前,我們經(jīng)常像下面這樣根據(jù)類型實現(xiàn),產(chǎn)生大量模板代碼:
func MaxInt(a, b int) int { if a > b { return a } return b } func MaxInt64(a, b int64) int64 { if a > b { return a } return b } // ...其他類型
而使用泛型則我們只需要一個實現(xiàn):
func Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b }
其中constraints.Ordered
表示可排序類型,也就是可以使用三路運算符的類型[>, =, <]
,包含了所有數(shù)值類型和string
。可以通過go get golang.org/x/exp
引入。
其他的像json解析、參數(shù)校驗、slices等也可以通過泛型進行實現(xiàn)。
數(shù)據(jù)結(jié)構(gòu)
Golang自帶的泛型容器有slices和map,這兩個數(shù)據(jù)結(jié)構(gòu)其實可以完成大部分工作了,但是有時候我們可能還需要其他的數(shù)據(jù)結(jié)構(gòu),比如說優(yōu)先級隊列、鏈表等。
雖然Golang在container
包下有heap
、list
和ring
三個數(shù)據(jù)結(jié)構(gòu),但說實話使用起來不是很方便,特別是元素類型全是interface{}
,使用這些結(jié)構(gòu)就需要各種類型轉(zhuǎn)換。因此我們可以簡單的拷貝這些代碼,然后使用泛型進行改造,比如heap:
我們不但使用泛型進行實現(xiàn),還把heap默認改為使用slice是實現(xiàn),這樣只需要實現(xiàn)一個LessFunc,而不是5個。
package heap type LessFunc[T any] func(e1 T, e2 T) bool type Heap[T any] struct { h []T lessFunc LessFunc[T] } func New[T any](h []T, lessFunc LessFunc[T]) *Heap[T] { heap := &Heap[T]{ h: h, lessFunc: lessFunc, } heap.init() return heap } // 移除堆頂元素 func (h *Heap[T]) Pop() T { n := h.Len() - 1 h.swap(0, n) h.down(0, n) return h.pop() } // 獲取堆頂元素 func (h *Heap[T]) Peek() T { return h.h[0] } // 添加元素到堆 func (h *Heap[T]) Push(x T) { h.push(x) h.up(h.Len() - 1) }
其他的數(shù)據(jù)結(jié)構(gòu)還包括list、set、pqueue等。
模板代碼
在后臺業(yè)務(wù)代碼里面,我們經(jīng)常會有很多個業(yè)務(wù)處理函數(shù),每個業(yè)務(wù)處理函數(shù)我們基本都會通過一些代碼封裝成一個HTTP接口,這里其實基本上都是模板代碼,比如說對于一個使用gin實現(xiàn)的HTTP
服務(wù),每個接口我們都需要進行以下處理:
- 指定HTTP方法、URL
- 鑒權(quán)
- 參數(shù)綁定
- 處理請求
- 處理響應(yīng)
可以發(fā)現(xiàn),參數(shù)綁定、處理響應(yīng)幾乎都是一樣模板代碼,鑒權(quán)也基本上是模板代碼(當然有些鑒權(quán)可能比較復(fù)雜)。
因此我們可以編寫一個泛型模板,把相同的部分抽取出來,用戶只需要實現(xiàn)不同接口有差異的指定HTTP方法、URL和處理請求邏輯即可:
// 處理請求 func do[Req any, Rsp any, Opt any](reqFunc ReqFunc[Req], serviceFunc ServiceFunc[Req, Rsp], serviceOptFunc ServiceOptFunc[Req, Rsp, Opt], opts ...Opt) gin.HandlerFunc { return func(c *gin.Context) { // 參數(shù)綁定 req, err := BindJSON[Req](c) if err != nil { return } // 進一步處理請求結(jié)構(gòu)體 if reqFunc != nil { reqFunc(c, req) } var rsp *Rsp // 業(yè)務(wù)邏輯函數(shù)調(diào)用 if serviceFunc != nil { rsp, err = serviceFunc(c, req) } else if serviceOptFunc != nil { rsp, err = serviceOptFunc(c, req, opts...) } else { panic("must set ServiceFunc or ServiceFuncOpt") } // 處理響應(yīng) ProcessRsp(c, rsp, err) } }
這樣,現(xiàn)在一個接口基本上只需要一行代碼即可實現(xiàn)(不包括具體業(yè)務(wù)邏輯函數(shù)):
// 簡單請求,不需要認證 e.GET("/user/info/get", ginrest.Do(nil, GetUserInfo)) // 認證,綁定UID,處理 reqFunc := func(c *gin.Context, req *UpdateUserInfoReq) { req.UID = GetUID(c) } // 這里拆多一步是為了顯示第一個參數(shù)是ReqFunc e.POST("/user/info/update", Verify, ginrest.Do(reqFunc, UpdateUserInfo))
代碼地址,實現(xiàn)了一個基于gin的RESTful風格模板。
對象池/緩存
Golang標準庫自帶了一個線程安全、高性能、還能夠根據(jù)對象熱度自動進行釋放的對象池sync.Pool
,然而作為對象池,我們一般只會往里面放一種類型的對象,但sync.Pool
里面的元素還是interface{}
類型,因此我們可以簡單的封裝sync.Pool
,讓它里面的元素有具體類型:
這里其實就是簡單的對象sync.Pool
進行包裝,然后添加了一個ClearFunc()
在回收對象的時候進行一些清理操作,比如說byte切片
我們需要讓它的已用長度歸零(容量還是不變)。
// 創(chuàng)建新對象 type NewFunc[T any] func() T // 清理對象 type ClearFunc[T any] func(T) T type Pool[T any] struct { p sync.Pool clearFunc ClearFunc[T] } func New[T any](newFunc NewFunc[T], clearFunc ClearFunc[T]) *Pool[T] { if newFunc == nil { panic("must be provide NewFunc") } p := &Pool[T]{ clearFunc: clearFunc, } p.p.New = func() any { return newFunc() } return p } // 獲取對象 func (p *Pool[T]) Get() T { return p.p.Get().(T) } // 歸還對象 func (p *Pool[T]) Put(t T) { if p.clearFunc != nil { t = p.clearFunc(t) } p.p.Put(t) }
作為字節(jié)數(shù)組對象池使用:
newFunc := func() []byte { return make([]byte, size, cap) } clearFunc := func(b []byte) []byte { return b[:0] } p := New(newFunc, clearFunc) bytes := p.Get() // 這里bytes類型是[]byte p.Put(bytes)
對于緩存也是同理,目前大部分緩存庫的實現(xiàn)都是基于interface{}
或者是byte[]
,但是我們還是更加喜歡直接操作具體類型,因此我們可以自己使用泛型實現(xiàn)(或改造)一個緩存庫。我自己也實現(xiàn)了一個泛型緩存策略庫,里面包含LRU、LFU、ARC、NearlyLRU、TinyLFU等緩存策略。
總結(jié)
可以看到,其實Golang泛型主要提供了一種代碼抽象、封裝的能力,讓我們能夠?qū)懗龈軓?fù)用的代碼,避免代碼到處拷貝,從而能夠提高代碼的可維護性,可讀性,還能從避免類型轉(zhuǎn)換中得到一點性能提升。
到此這篇關(guān)于詳解如何利用Golang泛型提高編碼效率的文章就介紹到這了,更多相關(guān)Golang泛型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用go實現(xiàn)刪除sql里面的注釋和字符串功能(demo)
這篇文章主要介紹了使用go實現(xiàn)刪除sql里面的注釋和字符串功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11golang實現(xiàn)簡易的分布式系統(tǒng)方法
這篇文章主要介紹了golang實現(xiàn)簡易的分布式系統(tǒng)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-10-10golang基于errgroup實現(xiàn)并發(fā)調(diào)用的方法
這篇文章主要介紹了golang基于errgroup實現(xiàn)并發(fā)調(diào)用,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-09-09go編程中g(shù)o-sql-driver的離奇bug解決記錄分析
這篇文章主要為大家介紹了go編程中g(shù)o-sql-driver的離奇bug解決記錄分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-05-05