基于Go 1.18使用泛型完成基础函数库重构
讲真,不管其他 Go粉
是怎么想的,对于我来说,Go
泛型在规划到1.18版本提案之后,我就一直留着哈喇子满怀期待。因为 泛型
这个东东真的是太想要啦。随着 Go 1.18
正式版发布,泛型也算是真正落地啦。接着我要使用这个新的特性重构一下目前团队中工具函数库。
由于这个库工具函数还比较多,所以这里摘两个和泛型相关的函数来分享下泛型解决的痛点:
// 检测某个元素是否存在数据组中
array.In(elem string, arr []string) => bool
// 数组元素去重
array.Unique(arr []string) => []string
array.In()
用来检测一个字符串数组中是否包含某个元素,返回一个 bool
类型变量;array.Unique()
用来对一个字符串数组(或切片)元素去重。像这种通用函数在团队实际开发中使用非常频繁,但是具体使用场景却是不局限于上面的字符串数据,可能实际上还需要 Int
,float64
类数组等等,以及其他类型。
可能玩其他语言的同学会好奇,怎么
Go
里这种类似的基础函数都要自己封装?确实,Go
不像其他语言,比如PHP
内置了很多方便的基础函数能拿来即用,Go
里面这些都需要自行实现。
泛型前的实现
这些函数很基础,但是又很通用,所以我们倾向于把它们设计为函数库,在泛型没出现之前,为了满足开发小伙伴的所有需求,我们的实现思路有两种:
方式一,把参数定义为 interface{}
万用类型:array.In(elem interface{}, arr interface{}),内部来判断具体传递什么类型。
// 元素定义为: interface{}
func In(elem interface{}, arr interface{}) bool {
switch val := elem.(type) { // 内部断言判断
case string:
tmpArr := arr.([]string)
for _, item := range tmpArr {
if val == item {
return true
}
}
case int:
// ...
default:
panic('')
}
return false
}
上面的实现能满足 In()
这个函数需求,但是要想满足 Unique()
函数这个有点难,因为 Uninue()
函数会有不同类型的返回,即我们期望的是:当传入参数为 []string
时,返回的时去重后的 []string
, 而传入的是 []int
时返回的是对应 []int
类型:
// 传字符串数组,返回字符串数组
array.Unique([]string{"A", "B", "B"}) ===> []string{"A", "B"}
// 传整数数组返回整数数组
array.Unique([]int{1, 1, 2}) ===> []int{1, 2}
如果你还想用返回 interface{}
方式来实现,那么使用方就需要一次强制类型转换,用起来很不舒服:
func Unique(interface{}) interface{} { // 返回interface{} 万用类型
// ...
}
使用方就需要自己强转:
val arrStr := array.Unique([]int{1,2,3,1}).([]int) // 自己强转,不优雅。
方式二:穷举定义若干相同类型的函数,然后使用不通的方法满足不通类型,就像 Go
官方介绍泛型举的例子那样,最终的代码大概长这样:
func UniqueInt(arr []int) []int { ... }
func UniqueInt8(arr []int8) []int8 { ... }
func UniqueInt16(arr []int16) []int16 { ... }
func UniqueInt32(arr []int32) []int32 { ... }
func UniqueInt64(arr []int64) []int64 { ... }
func UniqueString(arr []string) []string { ... }
// ...
以上代码实现,反正都不是很优雅,所以苦等泛型...
有了泛型之后
当泛型来了之后上面的痛点就迎刃而解了,还是拿刚才的函数来讲,我们使用泛型重构。
首先我们定一个新的元素类型 element
(叫什么无所谓)并约定它可以具体动态支持哪些类型数据约束(不定义新类型也是没问题的,这里是为了方便复用和让函数使用这些类时类型参数过长问题):
// 定义一个新的类型:element
type element interface {
// element 支持如下类型
string | int8 | int16 | int32 | int64 | int | float32 | float64 | uint | uint8 | uint16 | uint32 | uint64
}
然后我们使用新的类型参数来重新设计这个泛型函数:
func Unique[T element](arr []T) []T {}
和普通函数的区别是,函数名后多了一个中括号内容,以及形参和返回值都是未知类型T
中括号的[T elemet]
即 Go
语言泛型类型参数的设计实现,它把约束的类型参数放在中括号中 []
(不像 Java
用尖括号<>
)。
上面的函数我们声明了一个类型参数 T
(T
也是随便取名, 你可以任意发挥),并且它的类型约束为 element
类型,也即为(string | int ..
)类型,可以把 element
理解为上面那一堆类型的一个别名。返回值同样也是一个 []T
类型。
总的意思就是:我们定义了一个 Unique
函数,其输入类型为 element
下约束的类型,返回值也是如此。下面再贴出具体的函数体代码实现,内部其实把原先具体的类型换成了不确定的类型 T
,其它的和普通函数无差别:
// Unique 数组或slice边去重
func Unique[T element](arr []T) []T {
tmp := make(map[T]struct{})
l := len(arr)
if l == 0 {
return arr
}
rel := make([]T, 0, l)
for _, item := range arr {
_, ok := tmp[item]
if ok {
continue
}
tmp[item] = struct{}{}
rel = append(rel, item)
}
return rel[:len(tmp)]
}
测试
func TestUnique(t *testing.T) {
arrayInts := []int{1, 2, 2, 3, 4}
arrayStrs := []string{"A", "B", "C", "D", "C"}
// 测试整数
t.Log(array.Unique(arrayInts))
// 测试字符串
t.Log(array.Unique(arrayStrs))
}
得到结果如下:
main_test.go:22: [1 2 3 4]
main_test.go:23: [A B C D]
关于性能
因为泛型就相当于一个万类函数,那么我们使用普通的函数和泛型函数,在性能上有差异吗?答案是:没有什么差异,可以放心使用。
为了做测试,这里专门封装了一个普通 UniqueString
函数作对比:
func UniqueString(arr []string) []string {
tmp := make(map[string]struct{})
// ...
rel := make([]string, 0, l)
for _, item := range arr {
// ..
}
return rel[:len(tmp)]
}
func BenchmarkUniqueNotGeneric(b *testing.B) {
arrayStrs := []string{"A", "B", "C", "D", "C"}
for i := 0; i < b.N; i++ {
_ = array.UniqueString(arrayStrs)
}
}
func BenchmarkUniqueGeneric(b *testing.B) {
arrayStrs := []string{"A", "B", "C", "D", "C"}
for i := 0; i < b.N; i++ {
_ = array.Unique(arrayStrs)
}
}
普通函数和泛型参数设计一模一样,支持把类型换成了普通的字符串类型,下面是 benchmark
基准测试结果, 平均执行时间都在 116
纳秒附近,相差不大。
go test -bench=.
goos: darwin
goarch: arm64
pkg: generics_type
BenchmarkUniqueNotGeneric-10 8855962 116.7 ns/op
BenchmarkUniqueGeneric-10 10200286 115.6 ns/op
PASS
ok generics_type 4.008s
其它
因为泛型系统是 Go 1.18
才开始有的,所以使用 go module
开发时,你的 go.mod
文件的最低适配版本不能低于 1.18
,也就是一旦使用了泛型,你不在再想编译适配低于 1.18
版本的代码了。
module generics_type
go 1.18
require (...)
其外,目前 idea
+ Go插件
和 Goland
以及支持泛型开发,所以开发也比较顺手,后续我们会基于泛型做更多的项目实践。
转载自:https://juejin.cn/post/7075557057109164068