一个能让你少写循环和判断的Go开源包,支持泛型

科技   2024-09-04 08:52   北京  

大家在开发项目写代码的时候,最常用到的数据类型应该是列表,比如从数据库查询一个用户的订单,查询结果会以一个对象列表的形式返回给调用程序。

[]*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,它提供了很多好用的函数:ContainsDifferenceIndexOfFilterToMap等等,更多的可以参考它的网站: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真的很方便,还能链式调用,写起来很爽。可是轮到自己维护前人写的项目的时候,一来实际的业务逻辑本来就比书上的例子复杂,这些代码逻辑一顿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


最后的“要饭时刻”,真心推荐一下我前段时间写的付费专栏,该专栏主要用一些实际的案例讲解项目业务需求分析、技术实现分析、模块划分和分层的方法论,讲明白这些底层逻辑后再教大家怎么用 UML 等工具把它们图形化表达出来。详细内容请可扫码观看,或访问:独家原创--程序员的全能画图课

另外现在我在做的一个Go实战项目的专栏,整个项目也采用了这里讲的知识来实际地一步步做分析和开发的,大家可以先把这些方法论学会,后面跟着新专栏学习,看项目和代码的实践能明显提高自己做项目的架构水平。

网管叨bi叨
分享软件开发和系统架构设计基础、Go 语言和Kubernetes。
 最新文章