泛型

在Go 1.18版本中,添加了对泛型的支持。泛型是一种独立于所使用的特定类型的编写代码的方法。使用泛型可以编写出适用于一组类型中的任何一种的函数和类型,便利了代码的编写。

泛型的作用

假设我们有一个调换int参数的函数,现在我们需要一个调换float参数的函数,没有泛型时,我们需要将相同的逻辑、不同的类型参数的函数重复多遍。

1
2
3
4
5
6
7
func excInt(a, b int) (int, int) {
return b, a
}

func excFloat(a, b float32) (float32, float32) {
return b, a
}

但那是有了泛型,就可以很方便的编写出适用所有元素类型的“普适版”的函数

1
2
3
func exchange[T any](a, b T) (T, T) {
return b, a
}

泛型语法

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

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

这里我们着重讲类型参数

类型参数

在上面的例子中,我们提到可以使用泛型“很方便的编写出适用所有元素类型的“普适版”的函数”,这就是泛型的第一个作用——类型参数。

类型形参与类型实参

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

类型形参是作为可选类型,而在对函数实例化时,我们需要指定一个在可选类型范围内的类型实参。

类型实例化

在上面min函数中,同时支持了int和float64两种类型

1
2
m1 := min[int](1, 2)
m2 := min[float64](-0.1, -0.2)

向函数(min)提供类型实参(int、float64)被称之为实例化。在成功实例化后,我们会得到一个非泛型的函数,这个函数可以跟其他函数一样被正常调用

1
2
fmin := min[float64] // 类型实例化,编译器生成T=float64的min函数
m = fmin(1.2, 2.3)

类型参数的使用

除了可以在函数中使用泛型,在类型中也可以使用类型参数列表

1
2
3
4
5
6
7
8
9
10
11
//对切片封装一层
type Slice[T int | string] []T

//对map封装一层
type Map[K int | string, V float32 | float64] map[K]V

//二叉树的数据结构
type Tree[T interface{}] struct {
left, right *Tree[T]
value T
}

以上的类型都可以被称之为泛型类型。

泛型类型可以有方法,例如可以为上面的Tree添加一个寻找的方法

1
2
3
func (t *Tree[T])LookUp(x T) *Tree[T] {
//具体代码...
}

使用泛型类型必须先进行实例化

1
var stringTree Tree[string]

类型约束

类似参数列表中每个参数都有对应的参数类型,类型参数列表中的每一个参数都有对应的类型约束。

在Go语言中,实现类型约束的是接口类型。在上面的例子中,我们对于类型约束都省略了一个外层interface{},这是通常的写法,但实际上类型约束是一个接口类型

1
2
3
4
5
6
7
// 类型约束字面量,通常外层interface{}可省略
func min[T interface{ int | float64 }](a, b T) T {
if a <= b {
return a
}
return b
}

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

1
2
3
4
5
6
7
8
9
10
// 事先定义好的类型约束类型
type Value interface {
int | float64
}
func min[T Value](a, b T) T {
if a <= b {
return a
}
return b
}