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

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

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 等其他类型的切片的话该怎么办?很简单,给每种类型都定义个新类型:

type StringSlice []string
type Float32Slie []float32
type Float64Slice []float64

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

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)

泛型类型不能直接拿来使用,必须传入 类型实参(Type argument) 将其确定为具体的类型之后才可使用。而传入类型实参确定具体类型的操作被称为 实例化(Instantiations)

// 这里传入了类型实参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] ,被实例化之后的类型定义可近似视为如下:

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

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

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

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

上面只是个最简单的例子,实际上类型形参的数量可以远远不止一个,如下:

// 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,
}

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

  • KEYVALUE类型形参

  • int|stringKEY 的类型约束,float32|float64VALUE 的类型约束

  • KEY int|string, VALUE float32|float64 整个一串文本因为定义了所有形参所以被称为 类型形参列表

  • Map[KEY, VALUE]泛型类型,类型的名字就叫 Map[KEY, VALUE]

  • var a MyMap[string, float64] = xx 中的 stringfloat64类型实参,用于分别替换 KEYVALUE实例化 出了具体的类型 MyMap[string, float64]

还有点头晕?没事,的确一下子有太多概念了,这里用一张图就能简单说清楚:

image 2024 12 23 17 22 53 927
Figure 1. Go泛型概念一览

其他的泛型类型

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

// 一个泛型类型的结构体。可用 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

类型形参的互相套用

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

type WowStruct[T int | float32, S []T] struct {
    Data     S
    MaxValue T
    MinValue T
}

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

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

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

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

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

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

几种语法错误

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

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

    //✗ 错误。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{} 或加上逗号消除歧义(关于接口具体的用法会在后半篇提及)

    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{} 解决问题。

特殊的泛型类型

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

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 可以赋值给变量 ab,但 string 类型的字符串 “hello” 不能赋值给 c

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

泛型类型的套娃

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

// 先定义个泛型类型 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中套一个泛型类型Slice[T]
type WowMap[T int|string] map[string]Slice[T]
// 在map中套Slice[T]的另一种写法
type WowMap2[T Slice[int] | Slice[string]] map[string]T

类型约束的两种选择

观察下面两种类型约束的写法。

type WowStruct[T int|string] struct {
    Name string
    Data []T
}

type WowStruct2[T []int|[]string] struct {
    Name string
    Data T
}

仅限于这个例子,这两种写法和实现的功能其实是差不多的,实例化之后结构体相同。但是像下面这种情况的时候,我们使用前一种写法会更好:

type WowStruct3[T int | string] struct {
    Data     []T
    MaxValue T
    MinValue T
}

匿名结构体不支持泛型

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

testCase := struct {
        caseName string
        got      int
        want     int
    }{
        caseName: "test OK",
        got:      100,
        want:     100,
    }

那么匿名结构体能不能使用泛型呢?答案是不能,下面的用法是错误的:

testCase := struct[T int|string] {
        caseName string
        got      T
        want     T
    }[int]{
        caseName: "test OK",
        got:      100,
        want:     100,
    }

所以在使用泛型的时候我们只能放弃使用匿名结构体,对于很多场景来说这会造成麻烦(最主要麻烦集中在单元测试的时候,为泛型做单元测试会非常麻烦,这点我之后的文章将会详细阐述)