问题背景
Golang 在1.18 推出了新特性 泛型 Generics
- 为什么要有泛型
- 什么是泛型
- 如何使用泛型
使用的golang版本
go version go1.18.5 linux/amd6
为什么要有泛型从一个经典的问题开始说起,实现计算两数之和的函数
1 | func Add(a int, b int) int { |
很快就写完了,那么要计算浮点型或者字符串怎么办?
1 | func AddFloat32(a float32, b float32) float32 { |
重复代码太多,有没有办法只写一个函数实现它们
1 | func AddInterface(a, b interface{}) interface{} { |
上述函数有两个要求:
- 两者的类型要一样,否则无法相加
- 需要是函数支持的类型,否则无法相加
同时,也存在两个问题
- 需要利用反射进行类型判断写起来很繁琐
- 添加反射操作必然导致性能的降低
1 | func BenchmarkAddInterface(b *testing.B){ |
执行 go test -bench "Add*"
得到如下图所示,性能大约降低20倍
泛型就能很好的解决上述问题
1 | func AddT[T int | string | float32](a, b T) T { |
那么是否泛型的性能又是怎么样
1 | func BenchmarkAddInterface(b *testing.B){ |
得到的结果如下图,与固定类型的增加基本没有差别
泛型
泛型的基本概念
函数存在 形参(parameter) 和 实参(argument) 这一基本概念
1 | func Add(a int, b int) int { |
为了替代固定的 int 类型,Go 引入了 类型形参 与 类型实参,让一个函数获取了处理多种不同类型数据的能力,这种编程方式就叫做 泛型编程
1 | // 假设 T 是类型形参,在定义函数时它的类型是不确定的,类似占位符 |
类型形参(Type parameter):T
表示代表的具体类型并不确定,类似一个占位符(可以使用其他符号代替)
类型约束(Type constraint): 表示类型形参 T
只可以接收 int
或 float32
或 string
类型的实参
类型形参列表(type parameter list): T int | string | float32
这一整串定义了所有的类型形参
泛型类型(Generic type):类型定义中带 类型形参 的类型
类型实参(Type argument):泛型类型不能直接拿来使用,必须传入类型实参(Type argument) 将其确定为具体的类型之后才可使用。
实例化(Instantiations) :传入类型实参确定具体类型的操作被称为 实例化(Instantiations)
类型参数的声明紧随:
- 函数名之后
- 类型名之后。类型参数通过类型集进行约束。

类型集本质上就是接口,类型集可以作为类型参数的约束,一个接口也可以具有类型参数。

类型集的规则是:同行并集,不同行交集
使用方法
基本类型
1 | type Slice[T int|float32|float64 ] []T |
map 的使用
1 | // MyMap类型定义了两个类型形参 KEY 和 VALUE。分别为两个形参指定了不同的类型约束 |
KEY
和VALUE
是类型形参int|string
是KEY
的类型约束,float32|float64
是VALUE
的类型约束KEY int|string, VALUE float32|float64
整个一串文本因为定义了所有形参所以被称为类型形参列表Map[KEY, VALUE]
是泛型类型,类型的名字就叫Map[KEY, VALUE]
var a MyMap[string, float64] = xx
中的string
和float64
是类型实参,用于分别替换KEY
和VALUE
,实例化出了具体的类型MyMap[string, float64]
其他类型
-
struct 的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 一个泛型类型的结构体。可用 int 或 sring 类型实例化
type MyStruct[T int | string] struct {
Name string
Data T
}
//实例话的时候需要指定T的类型
var mystruct1 = []MyStruct[int]{
{
"1",
1,
}, {
"2",
2,
},
}
//✗ 错误。
var mystruct2 = []MyStruct[int]{
{
"1",
1,
}, {
"2",
"2", //cannot use "2" (untyped string constant) as int value in struct literal
},
} -
接口的泛型使用
1
2
3
4
5
6
7
8
9
10
11// 一个泛型接口(关于泛型接口在后半部分会详细讲解)
type IPrintData[T int | float32 | string] interface {
Print(data T)
}
//实现类型
func (m *MyStruct[string]) Print(data string) {
}
var _ IPrintData[string] = (*MyStruct[string])(nil) -
chan 的泛型使用
1
2
3
4// 一个泛型通道,可用类型实参 int 或 string 实例化
type MyChan[T int | string] chan T
var myChan = make(MyChan[int], 1)
类型的互相嵌套
1 | type WowStruct[T int | float32, S []T] struct { |
其他错误的使用
-
定义泛型类型的时候,基础类型不能只有类型形参,如下:
1
2// ✗ 错误,类型形参不能单独使用 cannot use a type parameter as RHS in type declaration
type CommonType[T int|string|float32] T -
当类型约束的一些写法会被编译器误认为是表达式时会报错。如下
1
2
3
4
5
6
7
8
9
10//✗ 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
type NewType[T *int] []T
// 上面代码再编译器眼中:它认为你要定义一个存放切片的数组,数组长度由 T 乘以 int 计算得到
type NewType [T * int][]T
//✗ 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作
type NewType2[T *int|*float64] []T
//✗ 错误。 undeclare name: T
type NewType2 [T (int)] []T为了避免这种误解,解决办法就是给类型约束包上
interface{}
或加上逗号消除歧义1
2
3
4
5
6
7
8type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T
// 如果类型约束中只有一个类型,可以添加个逗号消除歧义
type NewType3[T *int,] []T //没错,这样可以而 type NewType3[T *int] []T 不行
//✗ 错误。如果类型约束不止一个类型,加逗号是不行的, unexpected comma; expecting ]
type NewType4[T *int|*float32,] []T
特殊的泛型类型
1 | type Wow[T int | string] int |
这里虽然使用了类型形参,但因为类型定义是 type Wow[T int|string] int
,所以无论传入什么类型实参,实例化后的新类型的底层类型都是 int
。所以int类型的数字123可以赋值给变量a
和b
,但string
类型的字符串 “hello”
不能赋值给c
,没有什么具体意义,但可以让我们理解泛型类型的实例化的机制
泛型类型的套娃
1 | // 先定义个泛型类型 Slice[T] |
匿名结构体不支持泛型
1 | testCase := struct { |
那么匿名结构体能不能使用泛型呢?答案是不能,下面的用法是错误的:
1 | //✗ 错误。 expected expression |
在使用泛型的时候我们只能放弃使用匿名结构体,对于很多场景来说这会造成麻烦(最主要麻烦集中在单元测试的时候,为泛型做单元测试会非常麻烦)
泛型receiver
为泛型类型 MySlice[T]
添加了一个计算成员总和的方法 Sum()
。注意观察这个方法的定义
1 | type MySlice[T int | float32] []T |
- 首先看receiver
(s MySlice[T])
,直接把类型名称MySlice[T]
写入了receiver中 - 然后方法的返回参数我们使用了类型形参 T (方法的接收参数也可以实用类型形参)
- 在方法的定义中,我们也可以使用类型形参 T (在这个例子里,我们通过
var sum T
定义了一个新的变量sum
)
泛型类型无论如何都需要先用类型实参实例化!!!
1 | var s MySlice[int] = []int{1, 2, 3, 4} |
用类型实参 int 实例化了泛型类型 MySlice[T]
,所以泛型类型定义中的所有 T 都被替换为 int,最终我们可以把代码看作下面这样
1 | type MySlice[int] []int // 实例化后的类型名叫 MyIntSlice[int] |
基于泛型的队列
1 | // 这里类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型 Queue[T] (关于接口在后半部分会详细介绍) |
Queue[T]
因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:
1 | var q1 Queue[int] // 可存放int类型数据的队列 |
动态判断变量的类型
使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理
1 | var i interface{} = 123 |
那么对于 valut T
这样通过类型形参定义的变量,能不能判断具体类型然后对不同类型做出不同处理呢?答案是不允许的!!!
1 | // invalid operation: cannot use type assertion on type parameter value value (variable of type T constrained by interface{}) |
虽然type switch
和类型断言不能用,但我们可通过反射机制达到目的:
1 | func (receiver Queue[T]) Put(value T) { |
为了避免使用反射而选择了泛型,结果到头来又为了一些功能在在泛型中使用反射,当出现这种情况的时候你可能需要重新思考一下,自己的需求是不是真的需要用泛型(毕竟泛型机制本身就很复杂了,再加上反射的复杂度,增加的复杂度并不一定值得)
泛型函数
带类型形参的函数被称为泛型函数,匿名函数不支持泛型
1 | fn := func(a, b int) int { |
那么Go支不支持匿名泛型函数呢?答案是不能——匿名函数不能自己定义类型形参
1 | // 错误,匿名函数不能自己定义类型实参 |
但是匿名函数可以使用别处定义好的类型实参,如
1 | func MyFunc[T int | float32 | float64](a, b T) { |
泛型方法
Go的方法并不支持泛型
1 | type A struct { |
但是因为receiver支持泛型, 所以如果想在方法中使用泛型的话,目前唯一的办法就是曲线救国,迂回地通过receiver使用类型形参
1 | type A[T int | float32 | float64] struct { |
泛型使用进阶
复杂的接口
有时候使用泛型编程时,会书写长长的类型约束,如下
1 | // 一个可以容纳所有int,uint以及浮点类型的泛型切片 |
这种写法是无法忍受也难以维护的,而Go支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护
1 | type IntUintFloat interface { |
不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 |
进行组合
1 | type Int interface { |
在接口里也能直接组合其他接口
1 | type SliceElement interface { |
~ 符号
上面定义的 Slie[T]
虽然可以达到目的,但是有一个缺点:
1 | var s1 Slice[int] // 正确 |
错误原因:泛型类型 Slice[T]
允许的是 int
作为类型实参,而不是 MyInt (虽然 MyInt 类型底层类型是 int ,但它依旧不是 int 类型)。
为了从根本上解决这个问题,Go新增了一个符号 ~
,在类型约束中使用类似 ~int
这种写法的话,就代表着不光是 int ,所有以 int 为底层类型的类型也都可用于实例化
1 | type Int interface { |
限制:使用 ~
时有一定的限制:
~
后面的类型不能为接口~
后面的类型必须为基本类型
1 | type MyInt int |
接口的变化
从方法集(Method set)到类型集(Type set)
在Go1.18之前,Go官方对 接口(interface)
的定义是:接口是一个方法集(method set)
ReadWriter
接口定义了一个接口(方法集),这个集合中包含了 Read()
和 Write()
这两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口
An interface type specifies a method set called its interface
1 | type ReadWriter interface { |
换个角度重新理解
把
ReaderWriter
接口看成代表了一个 类型的集合,所有实现了Read()
Writer()
这两个方法的类型都在接口代表的类型集合当中
接口的定义就从 方法集(method set)
变为了 类型集(type set)
。而Go1.18开始就是依据这一点将接口的定义正式更改为了 类型集(Type set)
An interface type defines a *type set* (一个接口类型定义了一个类型集)
1 | type Float interface { |
接口类型 Float
代表了一个 类型集合, 所有以 float32
或 float64
为底层类型的类型,都在这一类型集之中
而 type Slice[T Float] []T
中, 类型约束 的真正意思是:指定了类型形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化
1 | var s Slice[int] // int 属于类型集 Float ,所以int可以作为类型实参 |
接口实现(implement)定义的变化
当满足以下条件时,可以说 类型 T 实现了接口 I ( type T implements interface I):
- T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员 (T is an element of the type set of I)
- T 是接口时: T 接口代表的类型集是 I 代表的类型集的子集(Type set of T is a subset of the type set of I)
类型的并集
一直使用的 |
符号就是求类型的并集( union
)
1 | type Uint interface { // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集 |
类型的交集
接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的 交集
1 | type AllInt interface { |
- 接口 A 代表的是 AllInt 与 Uint 的 交集,即
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
- 接口 B 代表的则是 AllInt 和 ~int 的交集,即
~int
很显然,~int 和 int 的交集只有int一种类型,所以接口C代表的类型集中只有int一种类型
空集
当多个类型的交集如下面 Bad
这样为空的时候, Bad
这个接口代表的类型集为一个空集:
1 | type Bad interface { |
没有任何一种类型属于空集。虽然 Bad 这样的写法是可以编译的,但实际上并没有什么意义
空接口和 any
接下来说一个特殊的类型集——空接口 interface{}
。因为,Go1.18开始接口的定义发生了改变,所以 interface{}
的定义也发生了一些变更:
空接口代表了所有类型的集合
所以,对于Go1.18之后的空接口应该这样理解:
- 虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个 空集
- 类型约束中指定 空接口 的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用 空接口 来做类型形参
1 | // 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参 |
因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go1.18开始提供了一个和空接口 interface{}
等价的新关键词 any
,用来使代码更简单:
1 | type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T |
实际上 any
的定义就位于Go语言的 builtin.go
文件中(参考如下), any
实际上就是 interaface{}
的别名(alias),两者完全等价
所以从 Go 1.18 开始,所有可以用到空接口的地方其实都可以直接替换为any,如:
1 | var s []any // 等价于 var s []interface{} |
comparable(可比较) 和 可排序(ordered)
golang中类型的比较情况
-
Boolean(布尔值)、Integer(整型)、Floating-point(浮点数)、Complex(复数)、String(字符)这些类型毫无疑问可以比较。
-
Poniter (指针) 可以比较:如果两个指针指向同一个变量,或者两个指针类型相同且值都为 nil,则它们相等。注意,指向不同的零大小变量的指针可能相等,也可能不相等。
-
Channel (通道)具有可比性:如果两个通道值是由同一个 make 调用创建的,则它们相等
1
2
3
4
5
6
7
8
9c1 := make(chan int, 2)
c2 := make(chan int, 2)
c3 := c1
fmt.Println(c3 == c1) // true
fmt.Println(c2 == c1) // false -
Interface (接口值)具有可比性:如果两个接口值具有相同的动态类型和相等的动态值,则它们相等。
-
当类型 X 的值具有可比性且 X 实现 T 时,非接口类型 X 的值 x 和接口类型 T 的值 t 具有可比性。如果 t 的动态类型与 X 相同且 t 的动态值等于 x,则它们相等。
-
如果所有字段都具有可比性,则 struct (结构体值)具有可比性:如果它们对应的非空字段相等,则两个结构体值相等。
-
如果 array(数组)元素类型的值是可比较的,则数组值是可比较的:如果它们对应的元素相等,则两个数组值相等
-
slice、map、function 这些是不可以比较的,但是也有特殊情况,那就是当他们值是 nil 时,可以与 nil 进行比较
对于一些数据类型,需要在类型约束中限制只接受能 !=
和 ==
对比的类型,如map
:
1 | // 错误。因为 map 中键的类型必须是可进行 != 和 == 比较的类型 |
所以Go直接内置了一个叫 comparable
的接口,它代表了所有可用 !=
以及 ==
对比的类型:
1 | type MyMap[KEY comparable, VALUE any] map[KEY]VALUE // 正确 |
comparable
比较容易引起误解的一点是很多人容易把他与可排序搞混淆。可比较指的是 可以执行 != ==
操作的类型,并没确保这个类型可以执行大小比较( >,<,<=,>=
)。如下
1 | type OhMyStruct struct { |
而可进行大小比较的类型被称为 Orderd
。目前Go语言并没有像 comparable
这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考Go官方包golang.org/x/exp/constraints
如何定义:
1 | // Ordered 代表所有可比大小排序的类型 |
这里虽然可以直接使用官方包 golang.org/x/exp/constraints ,但因为这个包属于实验性质的 x 包,今后可能会发生非常大变动,所以并不推荐直接使用
接口两种类型
1 | type ReadWriter interface { |
接口类型 ReadWriter
代表了一个类型集合,所有以 string
或 []rune 为底层类型
,并且实现了 Read()
Write()
这两个方法的类型都在 ReadWriter
代表的类型集当中,例如:
StringReadWriter
存在于接口 ReadWriter
代表的类型集中,而 BytesReadWriter
因为底层类型是 []byte
(既不是string
也是不[]rune
) ,所以它不属于 ReadWriter
代表的类型集
1 | // 类型 StringReadWriter 实现了接口 Readwriter |
定义一个 ReadWriter
类型的接口变量,然后接口变量赋值的时候不光要考虑到方法的实现,还必须考虑到具体底层类型?心智负担也太大了吧。是的,为了解决这个问题也为了保持Go语言的兼容性,Go1.18开始将接口分为了两种类型
- 基本接口(Basic interface)
- 一般接口(General interface)
基本接口
接口定义中如果只有方法的话,那么这种接口被称为基本接口(Basic interface)。这种接口就是Go1.18之前的接口,用法也基本和Go1.18之前保持一致。基本接口大致可以用于如下几个地方
-
最常用的,定义接口变量并赋值
1
2
3
4
5
6type MyError interface { // 接口中只有方法,所以是基本接口
Error() string
}
// 用法和 Go1.18之前保持一致
var err MyError = fmt.Errorf("hello world") -
基本接口因为也代表了一个类型集,所以也可用在类型约束中
1
2// io.Reader 和 io.Writer 都是基本接口,也可以用在类型约束中
type MySlice[T io.Reader | io.Writer] []Slice
一般接口(General interface)
如果接口内不光只有方法,还有类型的话,这种接口被称为 一般接口(General interface)
1 | type Uint interface { // 接口 Uint 中有类型,所以是一般接口 |
一般接口类型不能用来定义变量,只能用于泛型的类型约束中。所以以下的用法是错误的:
1 | type Uint interface { |
这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到Go1.18之前的代码,同时也极大减少了书写代码时的心智负担
泛型接口
所有类型的定义中都可以使用类型形参,所以接口定义自然也可以使用类型形参,观察下面这两个例子
1 | type DataProcessor[T any] interface { |
因为引入了类型形参,所以这两个接口是泛型类型。而泛型类型要使用的话必须传入类型实参实例化才有意义。所以尝试实例化一下这两个接口。因为 T
的类型约束是 any,所以可以随便挑一个类型来当实参(比如string)
1 | DataProcessor[string] |
经过实例化之后就好理解了, DataProcessor[string]
因为只有方法,所以它实际上就是个 基本接口(Basic interface),这个接口包含两个能处理string类型的方法。像下面这样实现了这两个能处理string类型的方法就算实现了这个接口
1 | type CSVProcessor struct { |
再用同样的方法实例化 DataProcessor2[T]
1 | DataProcessor2[string] |
DataProcessor2[string]
因为带有类型并集所以它是 一般接口(General interface),所以实例化之后的这个接口代表的意思是:
- 只有实现了
Process(string) string
和Save(string) error
这两个方法,并且以int
或struct{ Data interface{} }
为底层类型的类型才算实现了这个接口 - 一般接口(General interface) 不能用于变量定义只能用于类型约束,所以接口
DataProcessor2[string]
只是定义了一个用于类型约束的类型集
1 | // XMLProcessor 虽然实现了接口 DataProcessor2[string] 的两个方法,但是因为它的底层类型是 []byte,所以依旧是未实现 DataProcessor2[string] |
接口定义的种种限制规则
Go1.18从开始,在定义类型集(接口)的时候增加了非常多十分琐碎的限制规则,其中很多规则都在之前的内容中介绍过了,但剩下还有一些规则因为找不到好的地方介绍,所以在这里统一介绍下
-
用
|
连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):1
2
3
4
5
6type MyInt int
// 错误,MyInt的底层类型是int,和 ~int 有相交的部分
type _ interface {
~int | MyInt
}但是相交的类型中是接口的话,则不受这一限制:
1
2
3
4
5
6
7
8
9
10
11
12
13type MyInt int
type _ interface {
~int | interface{ MyInt } // 正确
}
type _ interface {
interface{ ~int } | MyInt // 也正确
}
type _ interface {
interface{ ~int } | interface{ MyInt } // 也正确
} -
类型的并集中不能有类型形参
1
2
3
4
5
6
7type MyInf[T ~int | ~string] interface {
~float32 | T // 错误。T是类型形参
}
type MyInf2[T ~int | ~string] interface {
T // 错误
} -
接口不能直接或间接地并入自己
1
2
3
4
5
6
7
8
9
10
11
12
13
14type Bad interface {
Bad // 错误,接口不能直接并入自己
}
type Bad2 interface {
Bad1
}
type Bad1 interface {
Bad2 // 错误,接口Bad1通过Bad2间接并入了自己
}
type Bad3 interface {
~int | ~string | Bad3 // 错误,通过类型的并集并入了自己
} -
接口的并集成员个数大于一的时候不能直接或间接并入
comparable
接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17type OK interface {
comparable // 正确。只有一个类型的时候可以使用 comparable
}
type Bad1 interface {
[]int | comparable // 错误,类型并集不能直接并入 comparable 接口
}
type CmpInf interface {
comparable
}
type Bad2 interface {
chan int | CmpInf // 错误,类型并集通过 CmpInf 间接并入了comparable
}
type Bad3 interface {
chan int | interface{comparable} // 理所当然,这样也是不行的
} -
带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19type _ interface {
~int | ~string | error // 错误,error是带方法的接口(一般接口) 不能写入并集中
}
type DataProcessor[T any] interface {
~string | ~[]byte
Process(data T) (newData T)
Save(data T) error
}
// 错误,实例化之后的 DataProcessor[string] 是带方法的一般接口,不能写入类型并集
type _ interface {
~int | ~string | DataProcessor[string]
}
type Bad[T any] interface {
~int | ~string | DataProcessor[T] // 也不行
}
泛型并不取代Go1.18之前用接口+反射实现的动态类型,在下面情景的时候非常适合使用泛型:当你需要针对不同类型书写同样的逻辑,使用泛型来简化代码是最好的 (比如你想写个队列,写个链表、栈、堆之类的数据结构)