基于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() 用来对一个字符串数组(或切片)元素去重。像这种通用函数在团队实际开发中使用非常频繁,但是具体使用场景却是不局限于上面的字符串数据,可能实际上还需要 Intfloat64 类数组等等,以及其他类型。

可能玩其他语言的同学会好奇,怎么 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 以及支持泛型开发,所以开发也比较顺手,后续我们会基于泛型做更多的项目实践。

猜你喜欢

转载自juejin.im/post/7075557057109164068