Go 泛型

本文将介绍 Go 中的泛型。

一、设计发展

2009 年 12 月 3 日,Go 团队的 Russ Cox 发表了一篇名为 The Generic Dilemma 的文章,列举了现有的 3 种泛型实现方案:

  • C 的泛型支持:
    • 不实现泛型
    • 避免引入复杂性
    • 影响程序员开发效率
  • C++ 的泛型支持:
    • 在编译时为每个类型生成一份单独的函数函数实现
    • 将产生大量的、多余的、重复的代码
  • Java 的泛型支持:
    • Java 泛型
    • 包含隐式的装箱拆箱,影响代码执行效率

自此,Go 团队开始了对 Go 泛型设计的探索,在 12 年的时间中,不断进行着方案的提出、修改、否定。

直至 2021 年 12 月 14 日,Go 1.18 beta1 版本发布,Go 正式支持泛型。

二、泛型语法

1
2
3
4
5
6
7
type constraint interface {
~int | string
}

func fun[P constraint, Q interface{~int | string}, R ~int | string](x P, y Q, z R) {

}

其中,

  • 类型参数:

    • P、Q、R 为类型参数
    • 类型参数列表用 [] 包裹
  • 类型约束:

    • aConstraintinterface{~int | ~string}~int | string 为类型约束

    • 类型约束规定了类型实参必须满足的条件

    • 类型约束通过接口定义

      因此,在 Go 1.18 之后,接口不仅可以规定应该实现的方法的集合,也可以规定可传入的泛型实参的类型的集合。

    • 类型约束支持简写,如 interface{~int | ~string}~int | string

    • 如果支持多种类型,应该将类型通过 | 联合

    • 如果支持的是类型本身,应该为 类型名;如果支持的是以该类型为底层类型的所有类型,应该为 ~类型名

三、使用示例

下面这段代码中,

  • 定义了泛型约束 Number,限制传入的类型为 int | int32 | float32 | float64
  • 定义了泛型方法 sort
  • 定义了泛型方法 toString
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
type Number interface {
~int | ~int32 | ~float32 | ~float64
}

func sort[N Number](list []N) {
for right := len(list) - 1; right > 0; right-- {
for i := 1; i <= right; i++ {
if list[i-1] > list[i] {
temp := list[i]
list[i] = list[i-1]
list[i-1] = temp
}
}
}
}

func toString[N Number](list []N) {
for i, e := range list {
print(e)
if i != len(list)-1 {
print(",")
} else {
println()
}
}
}

func main() {
list := []int{555, 2, 3, 4}
sort(list)
toString(list)

list2 := []float32{1.2, 4.2, 55}
sort(list2)
toString(list2)
}

四、泛型函数实例化

Go 会在编译阶段中对泛型函数进行实例化,具体过程如下:

  • 通过 显示传入 / 自动推断 得到类型实参

  • 判断类型实参是否满足类型约束

  • 将泛型函数实例化为具体函数,即生成一份适用于类型实参的普通函数

    • 当使用相同的类型实参对泛型函数进行多次调用时,Go 仅会实例化一次

    • Go 不会为每种类型实参实例化一个函数,而是以 GC Shape 为单位进行实例化,GC Shape 相同的不同类型实参将会共享同一个函数

      GC Shape 指的是类型在 Go 垃圾收集器中的表示,它由类型的大小、所需的对齐方式以及类型中包含指针的部分所决定

因此,在 Go 的泛型方案下,泛型函数不仅能正确生效,并且不会影响运行时性能。

参考