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