Go 泛型

2024-07-31 18:27:57 浏览数 (1)

Go 1.18版本增加了对泛型的支持

泛型运行程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型

在编写某些代码或数据结构时先不提供值的类型,而是之后再提供。

为什么需要泛型

这里假设需要定义一个反转切片的函数

代码语言:javascript复制
 func reverse(s []int) []int {
     l := len(s)
     r := make([]int, l)
 ​
     for i, e := range s {
         r[l-i-1] = e
     }
     return r
 }
 ​
 fmt.Println(reverse([]int{1, 2, 3, 4})) 

但这个函数只能接收[]int类型的参数,如果我们想支持[]float64类型的参数,我们就需要再定义一个reverseFloat64Slice函数。

一遍一遍地编写相同的功能是低效的,实际上这个反转切片的函数并不需要知道切片中元素的类型,但为了适用不同的类型我们把一段代码重复了很多遍。

Go1.18之前我们可以尝试使用反射去解决上述问题,但是使用反射在运行期间获取变量类型会降低代码的执行效率并且失去编译期的类型检查,同时大量的反射代码也会让程序变得晦涩难懂

代码语言:javascript复制
 package main
 ​
 import (
     "fmt"
     "reflect"
 )
 ​
 func reverseSlice(slice interface{}) interface{} {
     // 获取传入切片的反射值
     value := reflect.ValueOf(slice)
     if value.Kind() != reflect.Slice {
         panic("Input is not a slice")
     }
 ​
     // 创建一个新的反转后的切片
     reversed := reflect.MakeSlice(value.Type(), value.Len(), value.Len())
     for i := 0; i < value.Len(); i   {
         reversed.Index(i).Set(value.Index(value.Len() - 1 - i))
     }
 ​
     // 返回反转后的切片
     return reversed.Interface()
 }
 ​
 func main() {
     intSlice := []int{1, 2, 3, 4}
     reversedIntSlice := reverseSlice(intSlice).([]int)
     fmt.Println(reversedIntSlice)
 ​
     floatSlice := []float64{1.1, 2.2, 3.3, 4.4}
     reversedFloatSlice := reverseSlice(floatSlice).([]float64)
     fmt.Println(reversedFloatSlice)
 }
 ​

这样的场景就非常适合使用泛型。从Go1.18开始,使用泛型就能够编写出适用所有元素类型的“普适版”reverse函数。

代码语言:javascript复制
 func reverseWithGenerics[T any](s []T) []T {
     l := len(s)
     r := make([]T, l)
 ​
     for i, e := range s {
         r[l-i-1] = e
     }
     return r
 }

语法

泛型为Go语言添加了三个新的重要特性:

  1. 函数和类型的类型参数。
  2. 将接口类型定义为类型集,包括没有方法的类型。
  3. 类型推断,它允许在调用函数时在许多情况下省略类型参数。

类型参数

类型形参和类型实参

函数定义时可以指定形参,函数调用时需要传入实参。

现在,Go语言中的函数和类型支持添加类型参数。类型参数列表看起来像普通的参数列表,只不过它使用方括号([])而不是圆括号(())。

类型实例化

这次定义的min函数就同时支持intfloat64两种类型,也就是说当调用min函数时,我们既可以传入int类型的参数。

代码语言:javascript复制
 m1 := min[int](1, 2)  // 1

也可以传入float64类型的参数。

代码语言:javascript复制
 m2 := min[float64](-0.1, -0.2)  // -0.2

min 函数提供类型参数(在本例中为intfloat64)称为实例化( instantiation )。

类型实例化分两步进行:

  1. 首先,编译器在整个泛型函数或类型中将所有类型形参(type parameters)替换为它们各自的类型实参(type arguments)。
  2. 其次,编译器验证每个类型参数是否满足相应的约束。

在成功实例化之后,我们将得到一个非泛型函数,它可以像任何其他函数一样被调用。例如:

代码语言:javascript复制
 fmin := min[float64] // 类型实例化,编译器生成T=float64的min函数
 m2 = fmin(1.2, 2.3)  // 1.2

min[float64]得到的是类似我们之前定义的minFloat64函数——fmin,我们可以在函数调用中使用它。

类型参数的使用

除了函数中支持使用类型参数列表外,类型也可以使用类型参数列表。

代码语言:javascript复制
 type Slice[T int | string] []T
 ​
 type Map[K int | string, V float32 | float64] map[K]V
 ​
 type Tree[T interface{}] struct {
     left, right *Tree[T]
     value       T
 }

在上述泛型类型中,TKV都属于类型形参,类型形参后面是类型约束,类型实参需要满足对应的类型约束。

泛型类型可以有方法,例如为上面的Tree实现一个查找元素的Lookup方法。

代码语言:javascript复制
 func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }

要使用泛型类型,必须进行实例化。Tree[string]是使用类型实参string实例化 Tree 的示例。

代码语言:javascript复制
 var stringTree Tree[string]
类型约束

普通函数中的每个参数都有一个类型; 该类型定义一系列值的集合。例如,我们上面定义的非泛型函数minFloat64那样,声明了参数的类型为float64,那么在函数调用时允许传入的实际参数就必须是可以用float64类型表示的浮点数值。

类似于参数列表中每个参数都有对应的参数类型,类型参数列表中每个类型参数都有一个类型约束。类型约束定义了一个类型集——只有在这个类型集中的类型才能用作类型实参。

Go语言中的类型约束是接口类型。

就以上面提到的min函数为例,我们来看一下类型约束常见的两种方式。

类型约束接口可以直接在类型参数列表中使用。

代码语言:javascript复制
 // 类型约束字面量,通常外层interface{}可省略
 func min[T interface{ int | float64 }](a, b T) T {
     if a <= b {
         return a
     }
     return b
 }

作为类型约束使用的接口类型可以事先定义并支持复用。

代码语言:javascript复制
 // 事先定义好的类型约束类型
 type Value interface {
     int | float64
 }
 func min[T Value](a, b T) T {
     if a <= b {
         return a
     }
     return b
 }

在使用类型约束时,如果省略了外层的interface{}会引起歧义,那么就不能省略。例如:

代码语言:javascript复制
 type IntPtrSlice [T *int] []T  // T*int ?
 ​
 type IntPtrSlice[T *int,] []T  // 只有一个类型约束时可以添加`,`
 type IntPtrSlice[T interface{ *int }] []T // 使用interface{}包裹

总结

如果你发现自己多次编写完全相同的代码,而这些代码之间的唯一区别就是使用的类型不同,这个时候就应该考虑是否可以使用类型参数。

泛型和接口类型之间并不是替代关系,而是相辅相成的关系。泛型的引入是为了配合接口的使用,让我们能够编写更加类型安全的Go代码,并能有效地减少重复代码。

参考

https://www.liwenzhou.com/posts/Go/generics/

0 人点赞