是时候从 ahmetb/go-linq 升级到 samber/lo 了
golang开发中经常需要集合数据进行转换、过滤、汇总等。以前这些工作常用linq来完成,但golang1.18发布后内置了泛型的支持。 这时lo库就成了更好的选择
golang开发中经常需要对集合数据进行转换、过滤、汇总等。 虽然这类工作大部分可以用简单地循环搞定,但毕竟还是啰嗦。
在golang1.18以前,这些工作常用ahmetb/go-linq来完成。
但随着golang1.18的发布,其提供内置了泛型的支持。 这时samber/lo成了更好的选择。
linq 的特点
LINQ(language integrated query)这个概念来自 c#,linq 库借用了此思路,在golang中提高了操作对slice的开发效率。
注意:只是提高了开发效率,而不是运行效率。
由于之前语言不支持泛型,所以 linq 为了可以统一处理各种slice只能使用反射机制。 这就注定了运行效率不会太高。
并且,根据linq的作者 ahmetb 本人的反馈, linq 应该是不会升级以利用golang的泛型特性了。
虽然在内部使用了反射,但每个linq方法都有两个版本,一个版本以 interface{} 为参数,效率略高,使用时需要自己造型。另一个版本则提高了类型安全性,代价是速度更慢,实际上这个版本也是 ahmetb 推荐的使用方式。
linq提供了优雅的链式API, 使用起来相当流畅。 所以它是golang1.18之前的最佳选择。
lo 的特点
引用官方的说法:
samber/lo is a Lodash-style Go library based on Go 1.18+ Generics.
它的风格借鉴于大名鼎鼎的 JavaScript 库 lodash,构建于Golang的泛型之上。
所以它天然类型安全,具有更高的性能和更好的编译期检查。 如果对比查看两库的源码,你会发现 lo 的实现比 linq 简单得多。
只是由于没有额外的抽象,lo 就无法提供链式API了。
实例对比
下面用几个典型的使用场景来对比演示每个库的使用方法及并对其进行性能基准测试。
三个典型场景为:
Uniq,去重
Filter,过滤出可以有被13整除的数字
Map,把数字转换成字符串,生成新的slice
从下面示例代码可以看出,实现同样逻辑, lo 版本是最简洁的。而自己手工实现最麻烦。
package linq_lo
import (
"fmt"
"github.com/ahmetb/go-linq/v3"
"github.com/samber/lo"
)
// Uniq home brew uniq
func Uniq(l []int) []int {
m := make(map[int]struct{}, len(l))
result := make([]int, 0, len(l))
for _, v := range l {
if _, ok := m[v]; ok {
continue
}
m[v] = struct{}{}
result = append(result, v)
}
return result
}
// UniqLo uniq with lo
func UniqLo(l []int) []int {
return lo.Uniq(l)
}
// UniqLinq uniq with linq
func UniqLinq(l []int) []int {
var result []int
linq.From(l).Distinct().ToSlice(&result)
return result
}
func Filter(l []int) []int {
fn := func(x int) bool {
return x%13 == 0
}
var result []int
for _, v := range l {
if fn(v) {
result = append(result, v)
}
}
return result
}
func FilterLo(l []int) []int {
fn := func(x, _ int) bool {
return x%13 == 0
}
return lo.Filter(l, fn)
}
func FilterLinq(l []int) []int {
fn := func(x int) bool {
return x%13 == 0
}
var result []int
linq.From(l).WhereT(fn).ToSlice(&result)
return result
}
func Map(l []int) []string {
result := make([]string, len(l), len(l))
for i, v := range l {
result[i] = fmt.Sprint(v)
}
return result
}
func MapLo(l []int) []string {
fn := func(x, _ int) string {
return fmt.Sprint(x)
}
return lo.Map(l, fn)
}
func MapLinq(l []int) []string {
fn := func(x int) string {
return fmt.Sprint(x)
}
var result []string
linq.From(l).SelectT(fn).ToSlice(&result)
return result
}
再来看看3者的性能基准测试,当处理10000个元素的int slice时,benchmark结果如下:
> go test -bench=. -benchmem
goos: darwin
goarch: arm64
pkg: linq-lo
BenchmarkFilter-8 3471 301885 ns/op 259553 B/op 18 allocs/op
BenchmarkFilterLo-8 4089 272913 ns/op 259518 B/op 18 allocs/op
BenchmarkFilterLinq-8 48 22872626 ns/op 6548686 B/op 400047 allocs/op
BenchmarkMap-8 217 5520868 ns/op 3206601 B/op 199750 allocs/op
BenchmarkMapLo-8 213 5597522 ns/op 3206746 B/op 199748 allocs/op
BenchmarkMapLinq-8 34 32357045 ns/op 13022029 B/op 599798 allocs/op
BenchmarkUniq-8 424 2821845 ns/op 2205290 B/op 14 allocs/op
BenchmarkUniqLinq-8 94 10671209 ns/op 7530467 B/op 102220 allocs/op
BenchmarkUniqLo-8 429 2758828 ns/op 2205357 B/op 14 allocs/op
PASS
ok linq-lo 12.054s
可以看出手工实现与 lo 库接近,远远优于linq。
在速度方面,lo 是 linq 的 4-80 倍。而在内存分配方面,linq 与 lo 甚至有上万倍的差距。
结论
虽然本文只对几个非常片面的几个场景作了对比, 但是不难得出结论:
lo 可以用更简洁地实现业务逻辑
lo 能提供更好的编译期检查
lo 的性能要远远好于 linq
因此,在golang1.18发布以后,全面用 lo 替换 linq 就是很自然的选择了