浅谈Go1.18版本新特性——泛型
泛型 Generics: 引入了对使用参数化类型的泛型代码的新支持, 达到了算法可复用的目的。
两数求和,泛型函数的使用
假设我们要计算两个数的和,函数可以这样子写
func Add(a int, b int) int { return a + b }
上面的函数很简单,但是它只能计算int类型的和。如果我们想要计算uint32、float32的和就需要另外针对不同类型定义不同函数。代码如下:
func AddFloat32(a float32, b float32) float32 {
return a + b
}
func AddUint32(a uint32, b uint32) uint32 {
return a + b
}
在Go1.18之前是不支持泛型的,如果要支持多种类型的求和只能是写多个函数,在Go1.18之后就可以使用泛型写出更加简洁的函数了。
package main
import "fmt"
// 限制T 只接收int、float32、float64三种类型
func Add[T int | float32 | float64](a T, b T) T {
return a + b
}
func main() {
fmt.Println(Add[int](1,2))
fmt.Println(Add[float64](1.1, 2.2))
}
-
泛型的一些基础使用语法
-
类型形参、类型实参、类型约束和泛型类型
type IntSlice []int
var a IntSlice = []int{1, 2, 3} // 正确
var b IntSlice = []float32{1.0, 2.0, 3.0} // ✗ 错误,因为IntSlice的底层类型是[]int,浮点类型的切片无法赋值
这里定义了一个新的类型 IntSlice ,它的底层类型是 []int ,理所当然只有int类型的切片能赋值给 IntSlice 类型的变量。
接下来如果我们想要定义一个可以容纳 float32 或 string 等其他类型的切片的话该怎么办?很简单,给每种类型都定义个新类型:
type StringSlice []string
type Float32Slie []float32
type Float64Slice []float64
但是这样做的问题显而易见,它们结构都是一样的只是成员类型不同就需要重新定义这么多新类型。那么有没有一个办法能只定义一个类型就能代表上面这所有的类型呢?答案是可以的,这时候就需要用到泛型了:
type Slice[T int|float32|float64 ] []T
不同于一般的类型定义,这里类型名称 Slice 后带了中括号,对各个部分做一个解说就是:在定义Slice类型的时候 T 代表的具体类型并不确定,类似一个占位符。int|float32|float64 这部分被称为类型约束(Type constraint),中间的 | 的意思是告诉编译器,类型形参 T 只可以接收 int 或 float32 或 float64 这三种类型的实参。中括号里的 T int|float32|float64 这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为 类型形参列表(type parameter list),这里新定义的类型名称叫 Slice[T],这种类型定义的方式中带了类型形参,很明显和普通的类型定义非常不一样,所以我们将这种类型定义中带 类型形参 的类型,称之为 泛型类型(Generic type)。泛型类型不能直接拿来使用,必须传入类型实参(Type argument) 将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为实例化。
// 这里传入了类型实参int,泛型类型Slice[T]被实例化为具体的类型 Slice[int]
var a Slice[int] = []int{1, 2, 3}
fmt.Printf("Type Name: %T",a) //输出:Type Name: Slice[int]
// 传入类型实参float32, 将泛型类型Slice[T]实例化为具体的类型 Slice[string]
var b Slice[float32] = []float32{1.0, 2.0, 3.0}
fmt.Printf("Type Name: %T",b) //输出:Type Name: Slice[float32]
// ✗ 错误。因为变量a的类型为Slice[int],b的类型为Slice[float32],两者类型不同
a = b
// ✗ 错误。string不在类型约束 int|float32|float64 中,不能用来实例化泛型类型
var c Slice[string] = []string{"Hello", "World"}
// ✗ 错误。Slice[T]是泛型类型,不可直接使用必须实例化为具体的类型
var x Slice[T] = []int{1, 2, 3}
-
其他泛型类型的使用:
// MyMap类型定义了两个类型形参 KEY 和 VALUE。分别为两个形参指定了不同的类型约束
// 这个泛型类型的名字叫: MyMap[KEY, VALUE]
type MyMap[KEY int | string, VALUE float32 | float64] map[KEY]VALUE
// 用类型实参 string 和 flaot64 替换了类型形参 KEY 、 VALUE,泛型类型被实例化为具体的类型:MyMap[string, float64]
var a MyMap[string, float64] = map[string]float64{
"jack_score": 9.6,
"bob_score": 8.4,
}
// 一个泛型类型的结构体。可用 int 或 sring 类型实例化
type MyStruct[T int | string] struct {
Name string
Data T
}
// 一个泛型接口(关于泛型接口在后半部分会详细讲解)
type IPrintData[T int | float32 | string] interface {
Print(data T)
}
// 一个泛型通道,可用类型实参 int 或 string 实例化
type MyChan[T int | string] chan T
-
泛型类型约束的使用
有时候使用泛型编程时,我们会书写长长的类型约束,如下:
// 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T
理所当然,这种写法是我们无法忍受也难以维护的,而Go支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:
type IntUintFloat interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
type Slice[T IntUintFloat] []T
这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat 当中。需要指定类型约束的时候直接使用接口 IntUintFloat 即可。
不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 | 进行组合:
type Int interface {
int | int8 | int16 | int32 | int64
}
type Uint interface {
uint | uint8 | uint16 | uint32
}
type Float interface {
float32 | float64
}
type Slice[T Int | Uint | Float] []T // 使用 '|' 将多个接口类型组合
上面的代码中,我们分别定义了 Int, Uint, Float 三个接口类型,并最终在 Slice[T] 的类型约束中通过使用 | 将它们组合到一起。
同时,在接口里也能直接组合其他接口,所以还可以像下面这样:
type SliceElement interface {
Int | Uint | Float | string // 组合了三个接口类型并额外增加了一个 string 类型
}
type Slice[T SliceElement] []T
指定底层类型,上面定义的 Slie[T] 虽然可以达到目的,但是有一个缺点:
var s1 Slice[int] // 正确
type MyInt int
var s2 Slice[MyInt] // ✗ 错误。MyInt类型底层类型是int但并不是int类型,不符合 Slice[T] 的类型约束
这里发生错误的原因是,泛型类型 Slice[T] 允许的是 int 作为类型实参,而不是 MyInt (虽然 MyInt 类型底层类型是 int ,但它依旧不是 int 类型)。为了从根本上解决这个问题,Go新增了一个符号 ~ ,在类型约束中使用类似 ~int 这种写法的话,就代表着不光是 int ,所有以 int 为底层类型的类型也都可用于实例化。
使用 ~ 对代码进行改写之后如下:
type Int interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32
}
type Float interface {
~float32 | ~float64
}
type Slice[T Int | Uint | Float] []T
var s Slice[int] // 正确
type MyInt int
var s2 Slice[MyInt] // MyInt底层类型是int,所以可以用于实例化
type MyMyInt MyInt
var s3 Slice[MyMyInt] // 正确。MyMyInt 虽然基于 MyInt ,但底层类型也是int,所以也能用于实例化
type MyFloat32 float32 // 正确
var s4 Slice[MyFloat32]
限制:使用 ~ 时有一定的限制:
~后面的类型不能为接口
~后面的类型必须为基本类型
type MyInt int
type _ interface {
~[]byte // 正确
~MyInt // 错误,~后的类型必须为基本类型
~error // 错误,~后的类型不能为接口
}
-
如果T想要包含全部类型可以用any和interface{}表示,any和interface{}也是等价的:
var s []any // 等价于 var s []interface{}
var m map[string]any // 等价于 var m map[string]interface{}
func MyPrint(value any){
fmt.Println(value)
}
-
匿名函数和匿名结构体是不支持泛型的。
-
目前Go也是不支持范型方法的。
-
几种泛型的错误语法
-
定义泛型的时候,基础类型不能只有类型形参:
type CommonType[T int|string|float32] T
-
当类型约束的一些写法会被编译器误认为是表达式时会报错
//✗ 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
type NewType[T *int] []T
// 上面代码再编译器眼中:它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
type NewType [T * int][]T
//✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
type NewType2[T *int|*float64] []T
//✗ 错误
type NewType2 [T (int)] []T
为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义
type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T
// 如果类型约束中只有一个类型,可以添加个逗号消除歧义
type NewType3[T *int,] []T
//✗ 错误。如果类型约束不止一个类型,加逗号是不行的
type NewType4[T *int|*float32,] []T
因为上面逗号的用法限制比较大,这里推荐统一用 interface{} 解决问题
-
用 |连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):
type MyInt int
// 错误,MyInt的底层类型是int,和 ~int 有相交的部分
type _ interface {
~int | MyInt
}
类型的并集不能有类型形参:
type MyInf[T ~int | ~string] interface {
~float32 | T // 错误。T是类型形参
}
type MyInf2[T ~int | ~string] interface {
T // 错误
}
参考:The Go Programming Language Specification - The Go Programming Language