Go 1.18版本开始,引入了泛型支持,接下来跟着文章的节奏一起学习下。
1. 类型形参理解
函数的 形参(parameter) 只是类似占位符的东西并没有具体的值,只有我们调用函数传入实参(argument) 之后才有具体的值。
如果我们将 形参 实参 这个概念推广一下,给变量的类型也引入和类似形参实参的概念的话,在这里我们将其称之为 类型形参(type parameter) 和 类型实参(type argument)。
1 | // 假设 T 是类型形参,在定义函数时它的类型是不确定的,类似占位符 |
在上面这段伪代码中, T 被称为 类型形参(type parameter), 它不是具体的类型,在定义函数时类型并不确定。因为 T 的类型并不确定,所以我们需要像函数的形参那样,在调用函数的时候再传入具体的类型。这样我们不就能一个函数同时支持多个不同的类型了吗?在这里被传入的具体类型被称为 类型实参(type argument)。
通过引入 类型形参 和 类型实参 这两个概念,我们让一个函数获得了处理多种不同类型数据的能力,这种编程方式被称为 泛型编程。
2. 泛型
2.1 命名多了中括号的泛型类型
1 | type StringSlice []string |
那么有没有一个办法能只定义一个类型就能代表上面这所有的类型呢?
1 | type Slice[T string|float32|float64 ] []T |
T
就是上面介绍过的类型形参(Type parameter),在定义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) 将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为 实例化(Instantiations) 。
1 | package main |
- map 的 demo
1 | package main |
2.2 类型嵌套
1 | package main |
2.3 类型的套娃
1 | // 先定义个泛型类型 Slice[T] |
2.4 几种语法错误
- 定义泛型类型的时候,基础类型不能只有类型形参
1 | type CommonType1[T int | string | float32] T // 无法编译 |
- 当类型约束的一些写法会被编译器误认为是表达式时会报错。
1 | type NewType[T *int] []T // 错误, T *int会被编译器误认为是表达式 T乘以int,而不是int指针 |
为了避免这种误解,解决办法就是给类型约束包上 interface{}
或加上逗号消除歧义
1 | // 包上 interface |
- 特殊的泛型类型
1 | package main |
这个例子没有什么具体意义,但是可以让我们理解泛型类型的实例化的机制。
无论传入什么类型实参,实例化后的新类型的底层类型都是 int 。所以int类型的数字123可以赋值给变量a和b,但string类型的字符串 “hello” 不能赋值给c。
2.5 类型约束的两种写法
这两种写法和实现的功能其实是差不多的,实例化之后结构体相同。但是用第一种更好,扩展性更强。
1 | type WowStruct[T int|string] struct { |
3. 函数和方法
3.1 判断变量的类型
泛型类型定义的变量不能使用类型断言。
1 | type Queue[T interface{}] struct { |
虽然type switch和类型断言不能用,但我们可通过反射机制达到目的:
1 | func (receiver Queue[T]) Put(value T) { |
你为了避免使用反射而选择了泛型,结果到头来又为了一些功能在在泛型中使用反射。请重新思考一下,自己的需求是不是真的需要用泛型?
3.2 泛型函数
1 | func Add[T int | float32 | float64](a T, b T) T { |
或许你会觉得这样每次都要手动指定类型实参太不方便了。所以Go还支持类型实参的自动推导:
1 | Add(1, 2) // 1,2是int类型,编译请自动推导出类型实参T是int |
3.3 自定义类型的方法
定义了新的普通类型之后可以给类型添加方法。
1 | package main |
3.4 泛型方法
普通结构体不支持,除非结构体自己支持泛型。
1 | type A struct { |
4. 接口
4.1 底层类型
1 | package main |
为了从根本上解决这个问题,Go新增了一个符号 ~
,在类型约束中使用类似 ~int
这种写法的话,就代表着不光是 int ,所有以 int 为底层类型的类型也都可用于实例化。
1 | package main |
限制:使用 ~
时有一定的限制:
- ~后面的类型不能为接口
- ~后面的类型必须为基本类型
4.2 类型集(Type set)
在Go1.18之前,Go官方对 接口(interface)
的定义是:接口是一个方法集(method set)
1 | type ReadWriter interface { |
ReadWriter 接口定义了一个接口(方法集),这个集合中包含了 Read() 和 Write() 这两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口。
但是,我们如果换一个角度来重新思考上面这个接口的话,会发现接口的定义实际上还能这样理解:
我们可以把 ReaderWriter 接口看成代表了一个 类型的集合,所有实现了 Read() Writer() 这两个方法的类型都在接口代表的类型集合当中
1 | type Float interface { |
接口类型 Float 代表了一个 类型集合, 所有以 float32 或 float64 为底层类型的类型,都在这一类型集之中。
而 type Slice[T Float] []T 中, 类型约束 的真正意思是:类型约束 指定了类型形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化。
1 | package main |
接口实现(implement)定义的变化
Go1.18开始 接口实现(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)
4.3 类型并集和交集
- 并集
1 | type Uint interface { // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集 |
- 交集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Uint interface { // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
// 接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的 交集
type AllInt interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
AllInt
Uint
}
type B interface { // 接口B代表的类型集是 AllInt 和 ~int 的交集
AllInt
~int
}
上面这个例子中
接口 A 代表的是 AllInt 与 Uint 的 交集,即 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
接口 B 代表的则是 AllInt 和 int 的交集,即 `int`
1 | type C interface { |
- 空集
1 | type Bad interface { |
4.4 空接口和 any
空接口 interface{}
。因为,Go1.18开始接口的定义发生了改变,所以 interface{}
的定义也发生了一些变更:空接口代表了所有类型的集合。
对于Go1.18之后的空接口应该这样理解:
虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个 空集
类型约束中指定 空接口 的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用 空接口 来做类型形参
1 | // 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参 |
因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go1.18开始提供了一个和空接口 interface{}
等价的新关键词 any
,用来使代码更简单:
1 | type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T |
4.5 可比较和可排序
对于一些数据类型,我们需要在类型约束中限制只接受能 !=
和 ==
对比的类型,如map:
1 | // 错误。因为 map 中键的类型必须是可进行 != 和 == 比较的类型 |
所以Go直接内置了一个叫 comparable
的接口,它代表了所有可用 !=
以及 ==
对比的类型:(牛)
1 | type MyMap[KEY comparable, VALUE any] map[KEY]VALUE // 正确 |
comparable
比较容易引起误解的一点是很多人容易把他与可排序搞混淆。可比较指的是 可以执行 !=
==
操作的类型,并没确保这个类型可以执行大小比较( >,<,<=,>=
)。
而可进行大小比较的类型被称为 Orderd
。目前Go语言并没有像 comparable
这样直接内置对应的关键词。
4.6 基本接口和一般接口🔥
1 | type ReadWriter interface { |
接口类型 ReadWriter 代表了一个类型集合,所有以 string 或 []rune 为底层类型,并且实现了 Read() Write() 这两个方法的类型都在 ReadWriter 代表的类型集当中。这个接口要求实现者不仅要是 string
或 []rune
类型的变体,还需要实现 Read
和 Write
两个方法。
1 | package main |
Go1.18开始将接口分为了两种类型:
1. 基本接口(Basic 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
2type MySlice[T io.Reader] []Slice // 正确
type MySlice[T io.Reader | io.Writer] []Slice // 错误,不能用作并集
2. 一般接口(General interface)
如果接口内不光只有方法,还有类型的话,这种接口被称为 一般接口(General interface) ,如下例子都是一般接口:
1 | type Uint interface { // 接口 Uint 中有类型,所以是一般接口 |
一般接口类型不能用来定义变量,只能用于泛型的类型约束中。所以以下的用法是错误的:
1 | type Uint interface { |
这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到Go1.18之前的代码,同时也极大减少了书写代码时的心智负担。
4.7 泛型接口
接口定义自然也可以使用类型形参,下面两个接口是泛型类型。
1 | type DataProcessor[T any] interface { |
而泛型类型要使用的话必须传入类型实参实例化才有意义。所以我们来尝试实例化一下这两个接口。因为 T 的类型约束是 any,所以可以随便挑一个类型来当实参(比如string):
1 | DataProcessor[string] |
再用同样的方法实例化 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] |
4.8 接口定义的种种限制规则
用
|
连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22type MyInt int
// 错误,MyInt的底层类型是int,和 ~int 有相交的部分
type _ interface {
~int | MyInt
}
// 但是相交的类型中是接口的话,则不受这一限制:
type 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
14
15type 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
19
20type _ 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] // 也不行
}