Go泛型

First of all, the reason behind this decision was to keep the language simple, as it is Go’s philosophy.

自从将Go从1.17升级到1.18后,我用到的新特性最多的就是将interface{}替换成any

这里学习一下Go的泛型使用。

关于Go泛型的效率的说明:Go Generics: Holistic Thoughts on Performance。也可以参考:On Go’s Generics Implementation and Performance

文中对Performance的3个标准:

  • Raw: Grind the code against a benchmark, is it faster or slower? Does it play well with memory and the garbage collector?
  • Development: How hard is it to use, i.e. impact on developers’ performance.
  • Lifetime: Reliability? Understandable? Flexible? Enhancible?

结论:如果业务逻辑中需要使用到泛型,那么使用泛型比使用any效率要高很多。

Lifetime标准和Development标准来说:Go’s generics will almost certainly improve your development and lifetime performance,对Raw标准:if generics are used when you actually need them they should also be a performance boost.

泛型能实现的功能通过接口+反射也基本能实现。但是使用过反射的人都知道反射机制有很多问题:

  • 用起来麻烦
  • 失去了编译时的类型检查,不仔细写容易出错
  • 性能不太理想

而在泛型适用的时候,它能解决上面这些问题。但这也不意味着泛型是万金油,泛型有着自己的适用场景。这也是上面提到的,在需要泛型的场景下,泛型效率要比接口+反射的方式高。

当你疑惑是不是该用泛型的话,请记住下面这条经验:

如果你经常要分别为不同的类型写完全相同逻辑的代码,那么使用泛型将是最合适的选择。

Quick Start

any 无类型约束

先来看一个场景,如果我们要迭代一个元素切片并在控制台上打印每个元素,如果不使用泛型那么只能这样做:

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
package main

import "fmt"

func main() {
arr1 := []any{1, 2, 3, 4}
arr2 := []any{3.14, 2.18, 3.18988}
arr3 := []any{"hello", "world"}
PrintAnyThing(arr1)
PrintAnyThing(arr2)
PrintAnyThing(arr3)
}

func PrintAnyThing(arr []any) {
for _, elem := range arr {
switch o := elem.(type) {
case int:
fmt.Printf("%d\n", o)
case float64:
fmt.Printf("%f\n", o)
case string:
fmt.Printf("%s\n", o)
default:
fmt.Printf("%+v\n", o)
}
}
}

这无疑是很麻烦的,而且很影响效率(在装箱、取消装箱和反射方面付出代价)。

接下来我们就看看使用泛型的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
arr1 := []int{1, 2, 3, 4}
arr2 := []float64{3.14, 2.18, 3.18988}
arr3 := []string{"hello", "world"}
PrintAnyThing(arr1)
PrintAnyThing(arr2)
PrintAnyThing(arr3)
}

func PrintAnyThing[T any](arr []T) {
for _, item := range arr {
fmt.Println(item)
}
}

Constraints 类型约束

上面的泛型例子中,类型指定的是any,也就是任何类型都可以。

有一种方法可以通过使用约束来限制泛型函数可以接受的数据类型。例如,我们可能希望将允许的类型仅限于可以转换为字符串的数据类型。为此,我们使用函数 String () String 创建一个接口 Stringer,并使用通用数据类型 T Stringer。这样,函数将只接受实现 String ()字符串函数的类型

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
28
29
30
31
32
33
package main

import "fmt"

type Test struct {
firstName string
lastName string
}

type Stringer interface {
String() string
}

func (t Test) String() string {
return t.firstName + t.lastName
}

func main() {
test1 := Test{"张", "三"}
test2 := Test{"李", "四"}
//arr1 := []int{1, 2, 3, 4}
arr2 := []Test{test1, test2}
//arr3 := []string{"hello", "world"}
//PrintSomeThing(arr1) // 报错
PrintSomeThing(arr2)
//PrintSomeThing(arr3) // 报错
}

func PrintSomeThing[T Stringer](arr []T) {
for _, item := range arr {
fmt.Println(item)
}
}

comparable 可比较约束

我们可以声明一个可比较的泛型类型,将参数限制为可以进行==!=操作的类型。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
arrS := []string{"green", "red", "red", "green", "blue", "green"}
fmt.Println(Count(arrS, "red")) // 2
fmt.Println(Count(arrS, "blue")) // 1
arrI := []int{1, 2, 3, 4, 2, 3, 3}
fmt.Println(Count(arrI, 2)) // 2
fmt.Println(Count(arrI, 3)) // 3
}

func Count[T comparable](arr []T, number T) int {
count := 0
for _, item := range arr {
if item == number {
count++
}
}
return count
}

Several types 指定约束类型

我们可以声明一个只接受特定数据类型的函数。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
arr1 := []int{1, 2, 3, 4, 3, 2, 1}
arr2 := []uint{2, 3, 4}
arr3 := []float64{3.14, 2.16, -0.998}
fmt.Printf("%T %v\n", Sum(arr1), Sum(arr1)) // int 16
fmt.Printf("%T %v\n", Sum(arr2), Sum(arr2)) // uint 9
fmt.Printf("%T %v\n", Sum(arr3), Sum(arr3)) // float64 4.3020000000000005
}

func Sum[T int | uint | float32 | float64](arr []T) T {
var result T
for _, item := range arr {
result = result + item
}
return result
}

上面代码中,就指定了泛型能接受的类型为:intuintfloat32float64

除了上面这种方式,我们还可以提前声明一个包含数据类型列表的接口:

1
2
3
type Addable interface {
int | int32 | int64 | uint | uint8 | float32 | float64
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type Addable interface {
int | int32 | int64 | uint | uint8 | float32 | float64
}

func main() {
arr1 := []int{1, 2, 3, 4, 3, 2, 1}
arr2 := []uint{2, 3, 4}
arr3 := []float64{3.14, 2.16, -0.998}
fmt.Printf("%T %v\n", Sum(arr1), Sum(arr1)) // int 16
fmt.Printf("%T %v\n", Sum(arr2), Sum(arr2)) // uint 9
fmt.Printf("%T %v\n", Sum(arr3), Sum(arr3)) // float64 4.3020000000000005
}

func Sum[T Addable](arr []T) T {
var result T
for _, item := range arr {
result = result + item
}
return result
}

存在自定义类型和类型别名情况下泛型的使用

在看一个例子,在这个例子中将引入存在自定义类型类型别名的情况。(Go的类型别名和自定义类型

存在类型别名的情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type myInt = int // 类型别名

type Addable interface {
int | int32 | int64 | uint | uint8 | float32 | float64
}

func main() {
var m myInt = 3
fmt.Println(Add(m, 4)) // 7
}

func Add[T Addable](a, b T) T {
return a + b
}

可以发现代码运行并没有什么问题。

存在自定义类型的情况,会发现代码会报错:myInt does not implement Addable (possibly missing ~ for int in constraint Addable)。正如报错消息所说,我们可以通过在Addable接口中的类型声明之前添加 ~ 来解决这个问题。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type myInt int // 自定义类型

type Addable interface {
~int | int32 | int64 | uint | uint8 | float32 | float64
}

func main() {
var m myInt = 3
fmt.Println(Add(m, 4)) // 7
}

func Add[T Addable](a, b T) T {
return a + b
}

例子:返回两数之间最小的值

在go中string也是可以比较的,所以我们也把string类型包含进来

最后来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type findMin interface {
uint | uint8 | uint16 | uint32 | uint64 | int | int8 |
int16 | int32 | int64 | float32 | float64 | string
}

func main() {
fmt.Println(min(2, 3))
fmt.Println(min(9.99, 3.14159))
fmt.Println(min("qingbo", "where"))
}

func min[T findMin](a, b T) T {
if a < b {
return a
} else {
return b
}
}

泛型详细介绍

这一部分,我们参考**Go 1.18 泛型全面讲解:一篇讲清泛型的全部**这篇文章,对泛型相关理论概念详细介绍一遍。

如果要使用泛型,看上面的Quick Start即可。

Go1.18实现的泛型的方式,除了引入类型形参类型实参这两个重要和基本的概念外,还引入了非常多全新的概念:

  • 类型形参 (Type parameter)
  • 类型实参(Type argument)
  • 类型形参列表( Type parameter list)
  • 类型约束(Type constraint)
  • 实例化(Instantiations)
  • 泛型类型(Generic type)
  • 泛型接收器(Generic receiver)
  • 泛型函数(Generic function)

类型形参、类型实参、类型约束和泛型类型

观察下面这个简单的例子:

1
2
3
4
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 类型的变量。

接下来如果我们想要定义一个可以容纳 float32string 等其他类型的切片的话该怎么办?很简单,给每种类型都定义个新类型:

1
2
3
type StringSlice []string
type Float32Slie []float32
type Float64Slice []float64

但是这样做的问题显而易见,它们结构都是一样的只是成员类型不同就需要重新定义这么多新类型。那么有没有一个办法能只定义一个类型就能代表上面这所有的类型呢?答案是可以的,这时候就需要用到泛型了:

1
type Slice[T int | float32 | float64] []T

不同于一般的类型定义,这里类型名称 Slice 后带了中括号[],其中:

  • T 就是上面介绍过的类型形参(Type parameter),在定义Slice类型的时候 T 代表的具体类型并不确定,类似一个占位符
  • int|float32|float64 这部分被称为类型约束(Type constraint),中间的 | 的意思是告诉编译器,类型形参 T 只可以接收 intfloat32float64 这三种类型的实参
  • 中括号里的 T int|float32|float64 这一整串因为定义了所有的类型形参(在这个例子里只有一个类型形参T),所以我们称其为 类型形参列表(type parameter list)
  • 这里新定义的类型名称叫 Slice[T]

这种类型定义的方式中带了类型形参,很明显和普通的类型定义非常不一样,所以我们将这种类型定义中带 类型形参的类型,称之为泛型类型(Generic type)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这里传入了类型实参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}

对于上面的例子,我们先给泛型类型 Slice[T] 传入了类型实参 int ,这样泛型类型就被实例化为了具体类型 Slice[int] ,被实例化之后的类型定义可近似视为如下:

1
type Slice[int] []int     // 定义了一个普通的类型 Slice[int] ,它的底层类型是 []int

我们用实例化后的类型 Slice[int] 定义了一个新的变量 a ,这个变量可以存储int类型的切片。之后我们还用同样的方法实例化出了另一个类型 Slice[float32] ,并创建了变量 b

因为变量 a 和 b 就是具体的不同类型了(一个 Slice[int] ,一个 Slice[float32]),所以 a = b 这样不同类型之间的变量赋值是不允许的。

同时,因为 Slice[T] 的类型约束限定了只能使用 int 或 float32 或 float64 来实例化自己,所以 Slice[string] 这样使用 string 类型来实例化是错误的。

类型形参的数量可以远远不止一个,如下:

1
2
3
4
5
6
7
8
9
// 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,
}

用上面的例子重新复习下各种概念:

  • 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]

其他的泛型类型

所有类型定义都可使用类型形参,所以下面这种结构体以及接口的定义也可以使用类型形参:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 一个泛型类型的结构体。可用 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

类型形参的互相套用

类型形参是可以互相套用的,如下

1
2
3
4
5
type WowStruct[T int | float32, S []T] struct {
Data S
MaxValue T
MinValue T
}

这个例子看起来有点复杂且难以理解,但实际上只要记住一点:任何泛型类型都必须传入类型实参实例化才可以使用。所以我们这就尝试传入类型实参看看:

1
2
var ws WowStruct[int, []int]
// 泛型类型 WowStuct[T, S] 被实例化后的类型名称就叫 WowStruct[int, []int]

上面的代码中,我们为T传入了实参 int,然后因为 S 的定义是 []T ,所以 S 的实参自然是 []int 。经过实例化之后 WowStruct[T,S] 的定义类似如下:

1
2
3
4
5
6
// 一个存储int类型切片,以及切片中最大、最小值的结构体
type WowStruct[int, []int] struct {
Data []int
MaxValue int
MinValue int
}

因为 S 的定义是 []T ,所以 T 一定决定了的话 S 的实参就不能随便乱传了,下面这样的代码是错误的:

1
2
3
4
5
6
// 错误。S的定义是[]T,这里T传入了实参int, 所以S的实参应当为 []int 而不能是 []float32
ws := WowStruct[int, []float32]{
Data: []float32{1.0, 2.0, 3.0},
MaxValue: 3,
MinValue: 1,
}

几种语法错误

  1. 定义泛型类型的时候,基础类型不能只有类型形参,如下:

    1
    2
    // 错误,类型形参不能单独使用
    type CommonType[T int|string|float32] T
  2. 当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:

    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

    // 错误
    type NewType2 [T (int)] []T

    为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义(关于接口具体的用法会在后面提及)

    1
    2
    3
    4
    5
    6
    7
    8
    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{} 解决问题

特殊的泛型类型

这里讨论种比较特殊的泛型类型,如下:

1
2
3
4
5
type Wow[T int | string] int

var a Wow[int] = 123 // 编译正确
var b Wow[string] = 123 // 编译正确
var c Wow[string] = "hello" // 编译错误,因为"hello"不能赋值给底层类型int

这里虽然使用了类型形参,但因为类型定义是 type Wow[T int|string] int ,所以无论传入什么类型实参,实例化后的新类型的底层类型都是 int 。所以int类型的数字123可以赋值给变量a和b,但string类型的字符串 “hello” 不能赋值给c。

这个例子没有什么具体意义,但是可以让我们理解泛型类型的实例化的机制。

泛型类型的套娃

泛型和普通的类型一样,可以互相嵌套定义出更加复杂的新类型,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 先定义个泛型类型 Slice[T]
type Slice[T int|string|float32|float64] []T

// ✗ 错误。泛型类型Slice[T]的类型约束中不包含uint, uint8
type UintSlice[T uint|uint8] Slice[T]

// ✓ 正确。基于泛型类型Slice[T]定义了新的泛型类型 FloatSlice[T] 。FloatSlice[T]只接受float32和float64两种类型
type FloatSlice[T float32|float64] Slice[T]

// ✓ 正确。基于泛型类型Slice[T]定义的新泛型类型 IntAndStringSlice[T]
type IntAndStringSlice[T int|string] Slice[T]
// ✓ 正确 基于IntAndStringSlice[T]套娃定义出的新泛型类型
type IntSlice[T int] IntAndStringSlice[T]

在map中使用泛型:

1
2
3
4
5
6
7
// 先定义个泛型类型 Slice[T]
type Slice[T int|string|float32|float64] []T

// 在map中套一个泛型类型Slice[T]
type WowMap[T int|string] map[string]Slice[T]
// 在map中套Slice[T]的另一种写法
type WowMap2[T Slice[int] | Slice[string]] map[string]T

匿名结构体不支持泛型

我们有时候会经常用到匿名的结构体,并在定义好匿名结构体之后直接初始化:

1
2
3
4
5
6
7
8
9
testCase := struct {
caseName string
got int
want int
}{
caseName: "test OK",
got: 100,
want: 100,
}

但是注意:匿名结构体不能使用泛型!下面的用法是错误的:

1
2
3
4
5
6
7
8
9
testCase := struct[T int|string] {
caseName string
got T
want T
}[int]{
caseName: "test OK",
got: 100,
want: 100,
}

所以在使用泛型的时候我们只能放弃使用匿名结构体

泛型receiver

我们知道,定义了新的普通类型之后可以给类型添加方法。这对于泛型类型来说,也是可以添加方法的:

1
2
3
4
5
6
7
8
9
type MySlice[T int | float32] []T

func (s MySlice[T]) Sum() T {
var sum T
for _, value := range s {
sum += value
}
return sum
}

这个例子为泛型类型 MySlice[T] 添加了一个计算成员总和的方法 Sum()

使用方法如下:

1
2
3
4
5
var s MySlice[int] = []int{1, 2, 3, 4}
fmt.Println(s.Sum()) // 输出:10

var s2 MySlice[float32] = []float32{1.0, 2.0, 3.0, 4.0}
fmt.Println(s2.Sum()) // 输出:10.0

该如何理解上面的实例化?以int类型为例,首先我们用类型实参 int 实例化了泛型类型 MySlice[T],所以泛型类型定义中的所有 T 都被替换为 int,最终我们可以把代码看作下面这样:

1
2
3
4
5
6
7
8
9
10
type MySlice[int] []int // 实例化后的类型名叫 MyIntSlice[int]

// 方法中所有类型形参 T 都被替换为类型实参 int
func (s MySlice[int]) Sum() int {
var sum int
for _, value := range s {
sum += value
}
return sum
}

通过泛型receiver,泛型的实用性一下子得到了巨大的扩展。在没有泛型之前如果想实现通用的数据结构,诸如:堆、栈、队列、链表之类的话,我们的选择只有两个:

  1. 为每种类型写一个实现
  2. 使用any+反射

而有了泛型之后,我们就能非常简单地创建通用数据结构了。

基于泛型的队列

队列这种数据结构就不必多说了,先进先出。

这里我们用go的泛型实现一下通用数据类型的队列:

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
package queue

type Queue[T any] struct {
elements []T
}

// Push 将数据放入队列尾部
func (q *Queue[T]) Push(value T) {
q.elements = append(q.elements, value)
}

// Pop 从队列头部取出并从头部删除对应数据
func (q *Queue[T]) Pop() T {
var value T
if len(q.elements) == 0 {
return value
}
value = q.elements[0]
q.elements = q.elements[1:]
return value
}

// Size 返回队列当前长度
func (q *Queue[T]) Size() int {
return len(q.elements)
}

main函数如下:

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
package main

import (
"fmt"

"helloworld1.18/generics/queue"
)

func main() {
var q1 queue.Queue[int] // 可存放int类型数据的队列
q1.Push(1)
q1.Push(2)
q1.Push(3)
for q1.Size() > 0 {
fmt.Println(q1.Pop())
}

var q2 queue.Queue[string] // 可存放string类型数据的队列
q2.Push("a")
q2.Push("b")
q2.Push("c")
for q2.Size() > 0 {
fmt.Println(q2.Pop())
}
}

泛型函数

假设我们想要写一个计算两个数之和的函数:

1
2
3
func Add(a int, b int) int {
return a + b
}

这个函数理所当然只能计算int的和,而浮点的计算是不支持的。这时候我们可以像下面这样定义一个泛型函数:

1
2
3
func Add[T int | float32 | float64](a T, b T) T {
return a + b
}

上面就是泛型函数的定义。这种带类型形参的函数被称为泛型函数

使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
fmt.Println(Add(1, 2)) // 1,2是int类型,编译请自动推导出类型实参T是int
fmt.Println(Add(3.12, 3.14)) // 3.12, 3.14是浮点,编译请自动推导出类型实参T是float64
}

func Add[T int | float32 | float64](a T, b T) T {
return a + b
}

和泛型类型一样,泛型函数也是不能直接调用的,要使用泛型函数的话必须传入类型实参之后才能调用。

1
2
3
4
Add[int](1,2) // 传入类型实参int,计算结果为 3
Add[float32](1.0, 2.0) // 传入类型实参float32, 计算结果为 3.0

Add[string]("hello", "world") // 错误。因为泛型函数Add的类型约束中并不包含string

每次都要手动指定类型实参太不方便了,所以Go支持类型实参的自动推导,使用就像上面那样。

自动推导的写法就好像免去了传入实参的步骤一样,但请记住这仅仅只是编译器帮我们推导出了类型实参,实际上传入实参步骤还是发生了的。

匿名函数不支持泛型

在Go中我们经常会使用匿名函数,如:

1
2
3
4
5
fn := func(a, b int) int {
return a + b
} // 定义了一个匿名函数并赋值给 fn

fmt.Println(fn(1, 2)) // 输出: 3

但是匿名函数不能自己定义类型形参

1
2
3
4
5
6
// 错误,匿名函数不能自己定义类型实参
fnGeneric := func[T int | float32](a, b T) T {
return a + b
}

fmt.Println(fnGeneric(1, 2))

但是匿名函数可以使用别处定义好的类型实参,如:

1
2
3
4
5
6
7
func MyFunc[T int | float32 | float64](a, b T) {
// 匿名函数可使用已经定义好的类型形参
fn2 := func(i T, j T) T {
return i*2 - j*2
}
fn2(a, b)
}

方法不支持泛型

目前Go的方法并不支持泛型

1
2
3
4
5
6
7
type A struct {
}

// 不支持泛型方法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {
return a + b
}

由于receiver支持泛型, 所以如果想在方法中使用泛型的话,目前唯一的办法就是曲线救国,迂回地通过receiver使用类型形参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type A[T int | float32 | float64] struct {
}

// 方法可以使用类型定义中的形参 T
func (receiver A[T]) Add(a T, b T) T {
return a + b
}

// 用法:
var a A[int]
a.Add(1, 2)

var aa A[float32]
aa.Add(1.0, 2.0)

变得复杂的接口

有时候使用泛型编程时,我们会书写长长的类型约束,如下:

1
2
// 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T

理所当然,这种写法是我们无法忍受也难以维护的,而Go支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:

1
2
3
4
5
type IntUintFloat interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

type Slice[T IntUintFloat] []T

不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 | 进行组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Int interface {
int | int8 | int16 | int32 | int64
}

type Uint interface {
uint | uint8 | uint16 | uint32
}

type Float interface {
float32 | float64
}

type All interface {
Int | Uint | Float | string
}

type Slice[T All] []T

~ : 指定底层类型

上面定义的 Slie[T] 虽然可以达到目的,但是有一个缺点:

1
2
3
4
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 为底层类型的类型也都可用于实例化。

使用 ~ 对代码进行改写之后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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]

使用 ~ 时有一定的限制~后面的类型必须为基本类型,不能为接口!

interface:从方法集(Method set)到类型集(Type set)

上面的例子中,我们学习到了一种接口的全新写法,而这种写法在Go1.18之前是不存在的。这也就意味着,Go语言中 接口(interface) 这个概念在Go1.18后发生了非常大的变化。

在Go1.18之前,Go官方对 接口(interface) 的定义是:接口是一个方法集(method set)。

An interface type specifies a method set called its interface

而Go1.18开始就是依据这一点将接口的定义正式更改为了 类型集(Type set)

An interface type defines a type set

既然接口定义发生了变化,那么从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)

类型的并集、并集和空集

并集:之前一直使用的 | 符号就是求类型的并集( union ):

1
2
3
type Uint interface {  // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

交集:接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的交集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
}

type C interface { // 接口C代表的类型集是~int和int的交集
~int
int
}

在这个例子中:

  • 接口 A 代表的是 AllInt 与 Uint 的 交集,即 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
  • 接口 B 代表的则是 AllInt 和 ~int 的交集,即 ~int

空集:当多个类型的交集如下面 Bad 这样为空的时候, Bad 这个接口代表的类型集为一个空集

1
2
3
4
type Bad interface {
int
float32
}

类型intfloat32没有相交的类型,所以接口Bad代表的类型集为空。

没有任何一种类型属于空集。虽然 Bad 这样的写法是可以编译的,但实际上并没有什么意义

interface{}和any

从Go1.18开始,接口的定义发生了改变,所以 interface{} 的定义也发生了一些变更:

空接口代表了所有类型的集合

所以,对于Go1.18之后的空接口应该这样理解:

  1. 虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个空集
  2. 类型约束中指定空接口的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用空接口来做类型形参

因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go1.18开始提供了一个和空接口 interface{} 等价的新关键词 any ,用来使代码更简单。

any 实际上就是 interaface{} 的别名(alias),两者完全等价。

1
2
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}

所以从Go 1.18开始,所有可以用到空接口interface{}的地方其实都可以直接替换为any

如果愿意,项目迁移到 Go1.18 之后可以使用下面这行命令直接把整个项目中的interface{}全都替换成any。当然因为并不强制,所以到底是用interface{}还是any全看自己喜好。

1
gofmt -w -r 'interface{} -> any' ./...

Go语言项目中就曾经有人提出过把Go语言中所有interface{}替换成anyissue,然后因为影响范围过大过而且影响因素不确定,理所当然被驳回了

comparable(可比较) 和 可排序(ordered)

对于一些数据类型,我们需要在类型约束中限制只接受能 !=== 对比的类型,如map:

1
2
// 错误。因为 map 中键的类型必须是可进行 != 和 == 比较的类型
type MyMap[KEY any, VALUE any] map[KEY]VALUE

所以Go直接内置了一个叫comparable的接口,它代表了所有可用 != 以及 == 对比的类型:

1
type MyMap[KEY comparable, VALUE any] map[KEY]VALUE // 正确

comparable比较容易引起误解的一点是很多人容易把他与可排序搞混淆。可比较指的是可以执行!=== 操作的类型,并没确保这个类型可以执行大小比较( ><<=>=)。

比如struct,可以进行!=== 操作,但是不能进行 ><<=>=这些操作。

1
2
3
4
5
6
7
8
9
10
type OhMyStruct struct {
a int
}

var a, b OhMyStruct

a == b // 正确。结构体可使用 == 进行比较
a != b // 正确

a > b // 错误。结构体不可比大小

而可进行大小比较的类型被称为 Orderd 。目前Go语言并没有像 comparable 这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考Go官方包golang.org/x/exp/constraints 如何定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Ordered 代表所有可比大小排序的类型
type Ordered interface {
Integer | Float | ~string
}

type Integer interface {
Signed | Unsigned
}

type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Float interface {
~float32 | ~float64
}

💡 这里虽然可以直接使用官方包 golang.org/x/exp/constraints ,但因为这个包属于实验性质的 x 包,今后可能会发生非常大变动,所以并不推荐直接使用

接口两种类型

我们接下来再观察一个例子,这个例子是阐述接口是类型集最好的例子:

1
2
3
4
5
6
type ReadWriter interface {
~string | ~[]rune

Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}

用类型集的概念就能比较轻松理解这个接口的意思:接口类型 ReadWriter 代表了一个类型集合,所有以string[]rune为底层类型,并且实现了Read()Write()这两个方法的类型都在ReadWriter代表的类型集当中。

如下面代码中,StringReadWriter 存在于接口 ReadWriter 代表的类型集中,而 BytesReadWriter 因为底层类型是 []byte(既不是string也是不[]rune) ,所以它不属于 ReadWriter 代表的类型集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 类型 StringReadWriter 实现了接口 Readwriter
type StringReadWriter string

func (s StringReadWriter) Read(p []byte) (n int, err error) {
// ...
}

func (s StringReadWriter) Write(p []byte) (n int, err error) {
// ...
}

// 类型BytesReadWriter 没有实现接口 Readwriter
type BytesReadWriter []byte

func (s BytesReadWriter) Read(p []byte) (n int, err error) {
...
}

func (s BytesReadWriter) Write(p []byte) (n int, err error) {
...
}

这样看来,接口也变得太复杂了。要定义一个 ReadWriter 类型的接口变量,然后接口变量赋值的时候不光要考虑到方法的实现,还必须考虑到具体底层类型,这样心智负担也太大了。

为了解决这个问题也为了保持Go语言的兼容性,Go1.18开始将接口分为了两种类型:

  • 基本接口(Basic interface)
  • 一般接口(General interface)

基本接口(Basic interface)

简单来说,基本接口就是:接口里面要么全是方法,要么全是类型。

接口定义中如果只有方法的话,那么这种接口被称为基本接口(Basic interface)。这种接口就是Go1.18之前的接口,用法也基本和Go1.18之前保持一致。基本接口大致可以用于如下几个地方:

  • 最常用的,定义接口变量并赋值:

    1
    2
    3
    4
    5
    6
    type 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
2
3
4
5
6
7
8
9
10
type Uint interface { // 接口 Uint 中有类型,所以是一般接口
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type ReadWriter interface { // ReadWriter 接口既有方法也有类型,所以是一般接口
~string | ~[]rune

Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}

一般接口类型不能用来定义变量,只能用于泛型的类型约束中。所以以下的用法是错误的:

1
2
3
4
5
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

var uintInf Uint // 错误。Uint是一般接口,只能用于类型约束,不得用于变量定义

这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到Go1.18之前的代码,同时也极大减少了书写代码时的心智负担。

泛型接口

所有类型的定义中都可以使用类型形参,所以接口定义自然也可以使用类型形参,观察下面这两个例子:

1
2
3
4
5
6
7
8
9
10
11
type DataProcessor[T any] interface {
Process(oriData T) (newData T)
Save(data T) error
}

type DataProcessor2[T any] interface {
int | ~struct{ Data interface{} }

Process(data T) (newData T)
Save(data T) error
}

因为引入了类型形参,所以这两个接口是泛型类型。而泛型类型要使用的话必须传入类型实参实例化才有意义。所以我们来尝试实例化一下这两个接口。因为 T 的类型约束是any,所以可以随便挑一个类型来当实参(比如string):

1
2
3
4
5
6
7
DataProcessor[string]

// 实例化之后的接口定义相当于如下所示:
type DataProcessor[string] interface {
Process(oriData string) (newData string)
Save(data string) error
}

经过实例化之后就好理解了, DataProcessor[string] 因为只有方法,所以它实际上就是个 基本接口(Basic interface),这个接口包含两个能处理string类型的方法。像下面这样实现了这两个能处理string类型的方法就算实现了这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type CSVProcessor struct {
}

// 注意,方法中 oriData 等的类型是 string
func (c CSVProcessor) Process(oriData string) (newData string) {
....
}

func (c CSVProcessor) Save(oriData string) error {
...
}

// CSVProcessor实现了接口 DataProcessor[string] ,所以可赋值
var processor DataProcessor[string] = CSVProcessor{}
processor.Process("name,age\nbob,12\njack,30")
processor.Save("name,age\nbob,13\njack,31")

// 错误。CSVProcessor没有实现接口 DataProcessor[int]
var processor2 DataProcessor[int] = CSVProcessor{}

再用同样的方法实例化 DataProcessor2[T]

1
2
3
4
5
6
7
8
9
DataProcessor2[string]

// 实例化后的接口定义可视为
type DataProcessor2[T string] interface {
int | ~struct{ Data interface{} }

Process(data string) (newData string)
Save(data string) error
}

DataProcessor2[string] 因为带有类型并集所以它是 一般接口(General interface),所以实例化之后的这个接口代表的意思是:

  1. 只有实现了 Process(string) stringSave(string) error 这两个方法,并且以 intstruct{ Data interface{} } 为底层类型的类型才算实现了这个接口
  2. 一般接口(General interface) 不能用于变量定义只能用于类型约束,所以接口 DataProcessor2[string] 只是定义了一个用于类型约束的类型集
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// XMLProcessor 虽然实现了接口 DataProcessor2[string] 的两个方法,但是因为它的底层类型是 []byte,所以依旧是未实现 DataProcessor2[string]
type XMLProcessor []byte

func (c XMLProcessor) Process(oriData string) (newData string) {

}

func (c XMLProcessor) Save(oriData string) error {

}

// JsonProcessor 实现了接口 DataProcessor2[string] 的两个方法,同时底层类型是 struct{ Data interface{} }。所以实现了接口 DataProcessor2[string]
type JsonProcessor struct {
Data interface{}
}

func (c JsonProcessor) Process(oriData string) (newData string) {

}

func (c JsonProcessor) Save(oriData string) error {

}

// 错误。DataProcessor2[string]是一般接口不能用于创建变量
var processor DataProcessor2[string]

// 正确,实例化之后的 DataProcessor2[string] 可用于泛型的类型约束
type ProcessorList[T DataProcessor2[string]] []T

// 正确,接口可以并入其他接口
type StringProcessor interface {
DataProcessor2[string]

PrintString()
}

// 错误,带方法的一般接口不能作为类型并集的成员
type StringProcessor interface {
DataProcessor2[string] | DataProcessor2[[]byte]

PrintString()
}

接口定义的种种限制规则

从Go1.18开始,在定义类型集(接口)的时候增加了非常多十分琐碎的限制规则,这里统一介绍下:

  1. | 连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):

    1
    2
    3
    4
    5
    6
    type MyInt int

    // 错误,MyInt的底层类型是int,和 ~int 有相交的部分
    type _ interface {
    ~int | MyInt
    }

    但是相交的类型中是接口的话,则不受这一限制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    type MyInt int

    type _ interface {
    ~int | interface{ MyInt } // 正确
    }

    type _ interface {
    interface{ ~int } | MyInt // 也正确
    }

    type _ interface {
    interface{ ~int } | interface{ MyInt } // 也正确
    }
  2. 类型的并集中不能有类型形参

    1
    2
    3
    4
    5
    6
    7
    type MyInf[T ~int | ~string] interface {
    ~float32 | T // 错误。T是类型形参
    }

    type MyInf2[T ~int | ~string] interface {
    T // 错误
    }
  3. 接口不能直接或间接地并入自己

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    type Bad interface {
    Bad // 错误,接口不能直接并入自己
    }

    type Bad2 interface {
    Bad1
    }
    type Bad1 interface {
    Bad2 // 错误,接口Bad1通过Bad2间接并入了自己
    }

    type Bad3 interface {
    ~int | ~string | Bad3 // 错误,通过类型的并集并入了自己
    }
  4. 接口的并集成员个数大于1的时候不能直接或间接并入 comparable 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type 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} // 理所当然,这样也是不行的
    }
  5. 带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    type _ 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] // 也不行
    }

总结

Go这次引入泛型带入了挺大的复杂度,也增加了挺多比较零散琐碎的规则限制。泛型虽然很受期待,但实际上推荐的使用场景也并没有那么广泛,对于泛型的使用,我们应该遵守下面的规则:泛型并不取代Go1.18之前用接口+反射实现的动态类型,在下面情景的时候非常适合使用泛型:当你需要针对不同类型书写同样的逻辑,使用泛型来简化代码是最好的 (比如你想写个队列,写个链表、栈、堆之类的数据结构)。

参考资料:The Go Programming Language Specification