一個(gè)能讓你少寫循環(huán)和判斷的Go開源包,支持范型
大家在開發(fā)項(xiàng)目寫代碼的時(shí)候,最常用到的數(shù)據(jù)類型應(yīng)該是列表,比如從數(shù)據(jù)庫查詢一個(gè)用戶的訂單,查詢結(jié)果會(huì)以一個(gè)對象列表的形式返回給調(diào)用程序。
[]*Order {
&{
ID: 1,
OrderNo: "20240903628359373756980001"
...
},
...
}
有了結(jié)果集列表之后,大部分時(shí)候?yàn)榱藢?shí)現(xiàn)產(chǎn)品邏輯我們需要在這個(gè)列表的基礎(chǔ)上進(jìn)行判斷、篩選和加工出自己想要的數(shù)據(jù)集。
因?yàn)榱斜硎嵌鄠€(gè)同類數(shù)據(jù)的集合,這些操作都需要我們在遍歷列表的基礎(chǔ)上來完成,比如判斷 ID 為1 的訂單在不在列表中。
......
var exists bool
for _, order := range orders {
if order.ID == 1 {
exists = true
}
}
...
當(dāng)然,為了減少遍歷次數(shù),我們通常會(huì)先通過一次遍歷生成一個(gè)以數(shù)據(jù)主鍵為Key的哈希Map:
map[int64]Order {
"1": {
ID: 1,
OrderNo: "20240903628359373756980001"
...
}
}
列表和哈希Map在Go里的類型是Slice 和 Map,上面這些操作應(yīng)該是大家寫代碼的時(shí)候,差不多每天都會(huì)遇到的情況。比如,從Slice切片中查找一個(gè)元素的位置、查找一個(gè)元素是不是存在、查找所有滿足條件的元素,又比如獲取Map的所有key、所有的value、還有像上面說的把Slice 轉(zhuǎn)換成 Map。
這些操作在所有編程語言里都很常見,比如Javascript里數(shù)組的map、reduce、filter函數(shù),Java 的 Stream API在編程中都非常好用,但是遺憾的是Go標(biāo)準(zhǔn)庫沒有提供類似的功能。
為了不在每個(gè)函數(shù)里都寫一遍,很多項(xiàng)目里會(huì)編寫大量的工具函數(shù)來進(jìn)行Slice和Map數(shù)據(jù)的處理,相信你一定在自己寫過的項(xiàng)目里見過一個(gè)叫 util的包,里面寫了各種 InSlice, InArray, XXXInSlice 等等之類的工具函數(shù)。
當(dāng)然這些也不用每次做項(xiàng)目都寫一遍,大部分通用的可以先從老項(xiàng)目粘到新項(xiàng)目里去。。。但是Go以前不支持范性,這種工具函數(shù)針對項(xiàng)目用到的自定義類型都寫一遍也是一個(gè)問題,時(shí)間長了也需要人來維護(hù)。
還有一種方式是使用社區(qū)里經(jīng)過充分的測試、驗(yàn)證,并且經(jīng)常更新的開源庫,在Go 1.18 版本以前比較有名的函數(shù)庫是go-funk,它提供了很多好用的函數(shù):Contains、Difference、IndexOf、Filter、ToMap等等,更多的可以參考它的網(wǎng)站:https://github.com/thoas/go-funk
因?yàn)樗窃贕o1.18 以前出來的,所以不可避免的會(huì)用到反射來處理多類型適配的問題,舉個(gè)Contains,即判斷是不是 InSlice的例子,假如不用反射,要多類型使用,就得定義很多相似名稱的函數(shù)。
func ContainsInt(collection []int, x int) bool {
}
func ContainsString(collection []string, x string) bool {
}
這跟咱們自己寫工具函數(shù)就沒什么區(qū)別了,在Go語言的泛型支持之前,要解決這個(gè)問題就能用反射。假如你們現(xiàn)在的項(xiàng)目還在用Go1.18 以前的版本,又不想寫自己手寫那么多循環(huán)和判斷代碼,那還是就用go-funk吧。
Go 1.18 支持了范型以后,很快就有人用范型寫出了與go-funk功能相同的函數(shù)包,這個(gè)包就是今天要介紹的主角,它叫 lo,名字有點(diǎn)怪,但是簡介里已經(jīng)寫清楚了它的用途: A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)
一個(gè)基于 Go 1.18+ 的范型,提供 map, filter, contains, find... 等操作的,類似 JS Lodash 工具包風(fēng)格的工具包,哈哈哈,翻譯過來字兒有點(diǎn)多。
它是基于泛型實(shí)現(xiàn),沒有用到反射,效率更高,代碼也更簡潔。比如剛才說的Contains函數(shù),是這么實(shí)現(xiàn)的:
func Contains[T comparable](collection []T, element T) bool {
for i := range collection {
if collection[i] == element {
return true
}
}
return false
}
只需要 T 被約束為 comparable 的,就可以使用==符號(hào)進(jìn)行比較了,整體代碼非常簡單,如果你自己寫代碼的時(shí)候需要用到范型,可以先學(xué)習(xí)學(xué)習(xí)它源碼中對Go范型的各種使用。
接下來我給大家演示一些我們常用到的操作使用 lo 庫的工具函數(shù)時(shí)應(yīng)該怎么寫。
常用的Slice 和 Map 操作
首先 lo 庫里提供了非常多的關(guān)于 Slice、Map、String、Channel 的操作, 不過官方給的例子比較簡單都是針對Int、String 切片這樣基礎(chǔ)類型集合的操作,我這里給大家演示一些我們實(shí)際開發(fā)時(shí)會(huì)用到的關(guān)于[]*Order 這樣的自定義類型的Slice 和 Map 的操作。
Filter 篩選符合條件的子列表
假如我們有一個(gè)像下面這樣的訂單列表
[]*Order {
&{
ID: 1,
OrderNo: "20240903628359373756980001"
UserId: 255
...
},
...
}
我們要篩選出訂單列表中 UserId 字段值等于參數(shù) userId 的所有元素。
func FindUserOrders(orders []*Order, userId int64) []*Order {
userOrders := lo.Filter(orders, func(item *Order, index int) bool {
return item.UserId == userId
})
return userOrders
}
從訂單列表中提取出所有訂單ID
有的時(shí)候我們希望從列表中提取出所有ID,再去做進(jìn)一步的數(shù)據(jù)庫的 IN (idList) 的查詢,這個(gè)時(shí)候我們可以使用Map 函數(shù)。
orderIds := lo.Map(orders, func(item *Order, index int) int64 {
return item.ID
})
把列表轉(zhuǎn)換成Map
文章開頭提到過,很多時(shí)候?yàn)榱藴p少遍歷次數(shù)會(huì)有把列表轉(zhuǎn)換成以ID 為 Key Map的需求,這個(gè)時(shí)候我們可以使用 lo 庫的 SliceToMap 來實(shí)現(xiàn)
orderMap := lo.SliceToMap(orders, func(item *Order) (int64, *Order) {
return item.ID, item
})
讓列表按字段進(jìn)行分組
如果你想讓上面的訂單列表按照 UserId 分組歸類,變成一個(gè) Key 是 UserId 值是用戶所有訂單的列表的 Map
map[int64][]*Order{
255: [
&{
ID: 1,
OrderNo: "20240903628359373756980001"
UserId: 255
...
}
...
]
...
}
我們可以使用 lo 庫的GroupBy方法來實(shí)現(xiàn)
userOrderMap = lo.GroupBy(orders, func(item *Order) int64 {
return item.UserId
})
Reduce 求加和
比如我們要求所有訂單金額的總和,可以使用 lo.Reduce 函數(shù)
// 計(jì)算總價(jià)
totalPrice := lo.Reduce(orders, func(agg int, item *Order, index int) int {
return agg + item.PayMoney
}, 0)
多線程Foreach
lo 包里除了提供了 Foreach 功能函數(shù)來遍歷集合
lo.ForEach([]string{"hello", "world"}, func(x string, _ int) {
println(x)
})
除此之外還可以用多個(gè)goroutine 來進(jìn)行遍歷,不過要安裝它的一個(gè)子包。
import lop "github.com/samber/lo/parallel"
lop.ForEach([]string{"hello", "world"}, func(x string, _ int) {
println(x)
})
這個(gè)說實(shí)話我沒有用過,如果你有一個(gè)超大的集合要遍歷可以嘗試一下。
Map 的常用操作中
lo 包中也有很多 Map 類型的操作功能,像 Keys、UniqKeys、Values、UniqValues 等等,其實(shí)各種功能的名字基本上跟其他語言提供的庫函數(shù)的名字類似,相信通過我上面的演示后大家完全可以自己探索,找到自己需要的功能了。
關(guān)于 lo 庫更多的功能,大家參考它的官方文檔吧:https://github.com/samber/lo 接下來我說說使用它編碼時(shí)的一些建議。
東西雖好,可別貪杯
關(guān)于 lo 庫的使用,我覺得能用一個(gè)簡單循環(huán)實(shí)現(xiàn)的邏輯就不用小題大做地來使用 lo 庫里的功能了,假如像是上面舉例的情況那樣,自己寫代碼要循環(huán)加判斷再加額外的變量賦值才能搞定,建議是 lo 庫里的功能,確實(shí)能讓少寫代碼,而且讓整個(gè)代碼塊的嵌套層次不會(huì)深,整體看上去會(huì)簡潔一些。
另外我覺得是,這些功能不要嵌套著用,本來就是函數(shù)式編程的風(fēng)格,再嵌套著用就很難看懂了。
以前我學(xué)Java的時(shí)候,覺得Java那個(gè) Stream API真的很方便,還能鏈?zhǔn)秸{(diào)用,寫起來很爽。可是輪到自己維護(hù)前人寫的項(xiàng)目的時(shí)候,一來實(shí)際的業(yè)務(wù)邏輯本來就比書上的例子復(fù)雜,這些代碼邏輯一頓Stream API鏈?zhǔn)秸{(diào)用,再加上用了 lambda,那代碼看起來真的是每次都要讀很久才能明白是在干啥。
比如下面這段代碼,不看注釋、不翻翻Java的Stream 和 Lambda 語法你能看明白這塊代碼是做什么的嗎?
// 求兩個(gè)List, aList 與 bList 的交集
List<Person> intersections = aList
.stream().filter(
a -> bList.stream().map(Person::getId)
.anyMatch(
id -> Objects.equals(a.getId(), id)
)
).collect(Collectors.toList());
總結(jié)
今天介紹的lo庫大家可以嘗試用起來,等用習(xí)慣了確實(shí)能讓自己的代碼少寫不少循環(huán)加判斷的邏輯,整體風(fēng)格會(huì)更清爽,自己也能少寫一些代碼。如果你們還在用的Go版本不支持范型,可以嘗試用一下風(fēng)格類似的庫go-funk。