Go-14-泛型

问题背景

Golang 在1.18 推出了新特性 泛型 Generics

  1. 为什么要有泛型
  2. 什么是泛型
  3. 如何使用泛型

使用的golang版本 go version go1.18.5 linux/amd6

为什么要有泛型从一个经典的问题开始说起,实现计算两数之和的函数

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

很快就写完了,那么要计算浮点型或者字符串怎么办?

1
2
3
4
5
6
7
func AddFloat32(a float32, b float32) float32 {
return a + b
}

func AddString(a string, b string) string {
return a + b
}

重复代码太多,有没有办法只写一个函数实现它们

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
func AddInterface(a, b interface{}) interface{} {
at := reflect.TypeOf(a)
bt := reflect.TypeOf(b)
if at != bt {
return nil
}

switch a.(type) {
case int:
return a.(int) + b.(int)
case string:
return a.(string) + b.(string)
case float32:
return a.(float32) + b.(float32)
}
return nil
}

func AddInt(a, b int) int {
return a + b
}

fmt.Printf("no Generic1 %v \n", AddInterface(1, 2)) //3
fmt.Printf("no Generic2 %v \n", AddInterface("1", "2")) //"12"
fmt.Printf("no Generic3 %v \n", AddInterface(float32(0.1), float32(0.2))) //0.3

上述函数有两个要求:

  1. 两者的类型要一样,否则无法相加
  2. 需要是函数支持的类型,否则无法相加

同时,也存在两个问题

  1. 需要利用反射进行类型判断写起来很繁琐
  2. 添加反射操作必然导致性能的降低
1
2
3
4
5
6
7
8
9
10
11
func BenchmarkAddInterface(b *testing.B){
for n := 0; n < b.N; n++ {
AddInterface(30, 30) // run AddInterface(30, 30) b.N times
}
}

func BenchmarkAddInt(b *testing.B){
for n := 0; n < b.N; n++ {
AddInt(30, 30) // AddInt(30, 30) b.N times
}
}

执行 go test -bench "Add*" 得到如下图所示,性能大约降低20倍

generic_1

泛型就能很好的解决上述问题

1
2
3
4
5
6
7
func AddT[T int | string | float32](a, b T) T {
return a + b
}

fmt.Printf("Generic1 %v \n", AddT(1, 2)) //3
fmt.Printf("Generic2 %v \n", AddT("1", "2")) //"12"
fmt.Printf("Generic3 %v \n", AddT(float32(0.1), float32(0.2))) //0.3

那么是否泛型的性能又是怎么样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func BenchmarkAddInterface(b *testing.B){
for n := 0; n < b.N; n++ {
AddInterface(30, 30) // run AddInterface(30, 30) b.N times
}
}

func BenchmarkAddInt(b *testing.B){
for n := 0; n < b.N; n++ {
AddInt(30, 30) // AddInt(30, 30) b.N times
}
}

func BenchmarkAddT(b *testing.B){
for n := 0; n < b.N; n++ {
AddT(30, 30) // AddT(30, 30) b.N times
}
}

得到的结果如下图,与固定类型的增加基本没有差别

generic_2

泛型

泛型的基本概念

函数存在 形参(parameter)实参(argument) 这一基本概念

1
2
3
4
5
6
func Add(a int, b int) int {  
// 变量a,b是函数的形参 "a int, b int" 这一串被称为形参列表
return a + b
}

Add(100,200) // 调用函数时,传入的100和200是实参

为了替代固定的 int 类型,Go 引入了 类型形参类型实参,让一个函数获取了处理多种不同类型数据的能力,这种编程方式就叫做 泛型编程

1
2
3
4
// 假设 T 是类型形参,在定义函数时它的类型是不确定的,类似占位符
func AddT[T int | string | float32](a, b T) T {
return a + b
}

类型形参(Type parameter)T 表示代表的具体类型并不确定,类似一个占位符(可以使用其他符号代替)

类型约束(Type constraint): 表示类型形参 T 只可以接收 intfloat32string 类型的实参

类型形参列表(type parameter list): T int | string | float32这一整串定义了所有的类型形参

泛型类型(Generic type):类型定义中带 类型形参 的类型

类型实参(Type argument):泛型类型不能直接拿来使用,必须传入类型实参(Type argument) 将其确定为具体的类型之后才可使用。

实例化(Instantiations) :传入类型实参确定具体类型的操作被称为 实例化(Instantiations)

类型参数的声明紧随:

  • 函数名之后
  • 类型名之后。类型参数通过类型集进行约束。
generic_3

类型集本质上就是接口,类型集可以作为类型参数的约束,一个接口也可以具有类型参数。

generic_4

类型集的规则是:同行并集,不同行交集

使用方法

基本类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Slice[T int|float32|float64 ] []T

// 这里传入了类型实参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}
map 的使用
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|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]
其他类型
  1. struct 的使用

    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
    // 一个泛型类型的结构体。可用 int 或 sring 类型实例化
    type MyStruct[T int | string] struct {
    Name string
    Data T
    }

    //实例话的时候需要指定T的类型
    var mystruct1 = []MyStruct[int]{
    {
    "1",
    1,
    }, {
    "2",
    2,
    },
    }

    //✗ 错误。
    var mystruct2 = []MyStruct[int]{
    {
    "1",
    1,
    }, {
    "2",
    "2", //cannot use "2" (untyped string constant) as int value in struct literal
    },
    }
  2. 接口的泛型使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 一个泛型接口(关于泛型接口在后半部分会详细讲解)
    type IPrintData[T int | float32 | string] interface {
    Print(data T)
    }

    //实现类型
    func (m *MyStruct[string]) Print(data string) {

    }

    var _ IPrintData[string] = (*MyStruct[string])(nil)
  3. chan 的泛型使用

    1
    2
    3
    4
    // 一个泛型通道,可用类型实参 int 或 string 实例化
    type MyChan[T int | string] chan T

    var myChan = make(MyChan[int], 1)
类型的互相嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type WowStruct[T int | float32, S []T] struct {
Data S
MaxValue T
MinValue T
}

var wowStruct1 = &WowStruct[int, []int]{
Data: []int{},
MaxValue: 1,
MinValue: 1,
}

//✗ 错误。[]float32 dose not implement []int
var wowStruct2 = &WowStruct[int, []float32]{
Data: []float32{},
MaxValue: 1,
MinValue: 1,
}
其他错误的使用
  1. 定义泛型类型的时候,基础类型不能只有类型形参,如下:

    1
    2
    // ✗ 错误,类型形参不能单独使用 cannot use a type parameter as RHS in type declaration
    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

    //✗ 错误。 undeclare name: 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 NewType3[T *int] []T 不行

    //✗ 错误。如果类型约束不止一个类型,加逗号是不行的, unexpected comma; expecting ]
    type NewType4[T *int|*float32,] []T
特殊的泛型类型
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可以赋值给变量ab,但string类型的字符串 “hello” 不能赋值给c,没有什么具体意义,但可以让我们理解泛型类型的实例化的机制

泛型类型的套娃
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 先定义个泛型类型 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 //!!! T 直接代替了 Slice[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
10
//✗ 错误。 expected expression
testCase := struct[T int|string] {
caseName string
got T
want T
}[int]{
caseName: "test OK",
got: 100,
want: 100,
}

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

泛型receiver

为泛型类型 MySlice[T] 添加了一个计算成员总和的方法 Sum() 。注意观察这个方法的定义

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
}
  • 首先看receiver (s MySlice[T]) ,直接把类型名称 MySlice[T] 写入了receiver中
  • 然后方法的返回参数我们使用了类型形参 T (方法的接收参数也可以实用类型形参)
  • 在方法的定义中,我们也可以使用类型形参 T (在这个例子里,我们通过 var sum 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 实例化了泛型类型 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
}
基于泛型的队列
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
// 这里类型约束使用了空接口,代表的意思是所有类型都可以用来实例化泛型类型 Queue[T] (关于接口在后半部分会详细介绍)
type Queue[T interface{}] struct {
elements []T
}

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

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

value = q.elements[0]
q.elements = q.elements[1:]
return value, len(q.elements) == 0
}

// 队列大小
func (q Queue[T]) Size() int {
return len(q.elements)
}

Queue[T] 因为是泛型类型,所以要使用的话必须实例化,实例化与使用方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var q1 Queue[int]  // 可存放int类型数据的队列
q1.Put(1)
q1.Put(2)
q1.Put(3)
q1.Pop() // 1
q1.Pop() // 2
q1.Pop() // 3

var q2 Queue[string] // 可存放string类型数据的队列
q2.Put("A")
q2.Put("B")
q2.Put("C")
q2.Pop() // "A"
q2.Pop() // "B"
q2.Pop() // "C"

var q3 Queue[struct{Name string}]
var q4 Queue[[]int] // 可存放[]int切片的队列
var q5 Queue[chan int] // 可存放int通道的队列
var q6 Queue[io.Reader] // 可存放接口的队列
// ......
动态判断变量的类型

使用接口的时候经常会用到类型断言或 type swith 来确定接口具体的类型,然后对不同类型做出不同的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
var i interface{} = 123
i.(int) // 类型断言

// type switch
switch i.(type) {
case int:
// do something
case string:
// do something
default:
// do something
}
}

那么对于 valut T 这样通过类型形参定义的变量,能不能判断具体类型然后对不同类型做出不同处理呢?答案是不允许的!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// invalid operation: cannot use type assertion on type parameter value value (variable of type T constrained by interface{})
func (q *Queue[T]) Put(value T) {
value.(int) // 错误。泛型类型定义的变量不能使用类型断言

// 错误。不允许使用type switch 来判断 value 的具体类型
// cannot use type switch on type parameter value value (variable of type T constrained by interface{})
switch value.(type) {
case int:
// do something
case string:
// do something
default:
// do something
}

// ...
}

虽然type switch和类型断言不能用,但我们可通过反射机制达到目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (receiver Queue[T]) Put(value T) {
// Printf() 可输出变量value的类型(底层就是通过反射实现的)
fmt.Printf("%T", value)

// 通过反射可以动态获得变量value的类型从而分情况处理
v := reflect.ValueOf(value)

switch v.Kind() {
case reflect.Int:
// do something
case reflect.String:
// do something
}

// ...
}

为了避免使用反射而选择了泛型,结果到头来又为了一些功能在在泛型中使用反射,当出现这种情况的时候你可能需要重新思考一下,自己的需求是不是真的需要用泛型(毕竟泛型机制本身就很复杂了,再加上反射的复杂度,增加的复杂度并不一定值得)

泛型函数

带类型形参的函数被称为泛型函数,匿名函数不支持泛型

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

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

那么Go支不支持匿名泛型函数呢?答案是不能——匿名函数不能自己定义类型形参

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

fmt.Println(fnGeneric(1, 2))

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

1
2
3
4
5
6
7
8
9
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
8
type A struct {
}

// 不支持泛型方法
// method must have no type parameters
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
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 // 使用 '|' 将多个接口类型组合

在接口里也能直接组合其他接口

1
2
3
4
5
type SliceElement interface {
Int | Uint | Float | string // 组合了三个接口类型并额外增加了一个 string 类型
}

type Slice[T SliceElement] []T
~ 符号

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

1
2
3
4
5
var s1 Slice[int] // 正确 

type MyInt int
// MyInt does not implement int|string|float32|float64 (possibly missing ~ for int in constraint int|string|float32|float64)
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]

限制:使用 ~ 时有一定的限制:

  1. ~后面的类型不能为接口
  2. ~后面的类型必须为基本类型
1
2
3
4
5
6
7
type MyInt int

type _ interface {
~[]byte // 正确
~MyInt // 错误,~后的类型必须为基本类型 invalid use of ~ (underlying type of MyInt is int)
~error // 错误,~后的类型不能为接口 invalid use of ~ (error is an interface)
}

接口的变化

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

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

ReadWriter 接口定义了一个接口(方法集),这个集合中包含了 Read()Write() 这两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口

An interface type specifies a method set called its interface

1
2
3
4
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}

换个角度重新理解

ReaderWriter 接口看成代表了一个 类型的集合,所有实现了 Read() Writer() 这两个方法的类型都在接口代表的类型集合当中

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

An interface type defines a *type set* (一个接口类型定义了一个类型集)

1
2
3
4
5
type Float interface {
~float32 | ~float64
}

type Slice[T Float] []T

接口类型 Float 代表了一个 类型集合, 所有以 float32 float64 为底层类型的类型,都在这一类型集之中

type Slice[T Float] []T 中, 类型约束 的真正意思是:指定了类型形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化

1
2
3
var s Slice[int]      // int 属于类型集 Float ,所以int可以作为类型实参
// chan int does not implement int|string|float32|float64
var s Slice[chan int] // chan int 类型不在类型集 Float 中,所以错误
接口实现(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

很显然,~int 和 int 的交集只有int一种类型,所以接口C代表的类型集中只有int一种类型

空集

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

1
2
3
4
type Bad interface {
int
float32
} // 类型 int 和 float32 没有相交的类型,所以接口 Bad 代表的类型集为空

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

空接口和 any

接下来说一个特殊的类型集——空接口 interface{} 。因为,Go1.18开始接口的定义发生了改变,所以 interface{} 的定义也发生了一些变更:

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

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

  1. 虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个 空集
  2. 类型约束中指定 空接口 的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用 空接口 来做类型形参
1
2
3
4
5
6
7
// 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参
type Slice[T interface{}] []T

var s1 Slice[int] // 正确
var s2 Slice[map[string]string] // 正确
var s3 Slice[chan int] // 正确
var s4 Slice[interface{}] // 正确

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

1
type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T

实际上 any 的定义就位于Go语言的 builtin.go 文件中(参考如下), any 实际上就是 interaface{}的别名(alias),两者完全等价

generic_6

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

1
2
3
4
5
6
var s []any // 等价于 var s []interface{}
var m map[string]any // 等价于 var m map[string]interface{}

func MyPrint(value any){
fmt.Println(value)
}
comparable(可比较) 和 可排序(ordered)

golang中类型的比较情况

  • Boolean(布尔值)、Integer(整型)、Floating-point(浮点数)、Complex(复数)、String(字符)这些类型毫无疑问可以比较。

  • Poniter (指针) 可以比较:如果两个指针指向同一个变量,或者两个指针类型相同且值都为 nil,则它们相等。注意,指向不同的零大小变量的指针可能相等,也可能不相等。

  • Channel (通道)具有可比性:如果两个通道值是由同一个 make 调用创建的,则它们相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    c1 := make(chan int, 2) 

    c2 := make(chan int, 2)

    c3 := c1

    fmt.Println(c3 == c1) // true

    fmt.Println(c2 == c1) // false
  • Interface (接口值)具有可比性:如果两个接口值具有相同的动态类型和相等的动态值,则它们相等。

  • 当类型 X 的值具有可比性且 X 实现 T 时,非接口类型 X 的值 x 和接口类型 T 的值 t 具有可比性。如果 t 的动态类型与 X 相同且 t 的动态值等于 x,则它们相等。

  • 如果所有字段都具有可比性,则 struct (结构体值)具有可比性:如果它们对应的非空字段相等,则两个结构体值相等。

  • 如果 array(数组)元素类型的值是可比较的,则数组值是可比较的:如果它们对应的元素相等,则两个数组值相等

  • slice、map、function 这些是不可以比较的,但是也有特殊情况,那就是当他们值是 nil 时,可以与 nil 进行比较

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type OhMyStruct struct {
a int
}

var a, b OhMyStruct

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

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

func Index[E comparable](s []E, v E) int {
for i, vs := range s {
if v == vs { return i }
}
return -1
}

而可进行大小比较的类型被称为 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
21
22
23
24
25
26
27
// 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
}

func IsSorted[E constraints.Ordered](x []E) bool {
for i := len(x) - 1; i > 0; i-- {
if x[i] < x[i-1] { return false }
}
return true
}

这里虽然可以直接使用官方包 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)。这种接口就是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. 接口的并集成员个数大于一的时候不能直接或间接并入 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] // 也不行
    }

泛型并不取代Go1.18之前用接口+反射实现的动态类型,在下面情景的时候非常适合使用泛型:当你需要针对不同类型书写同样的逻辑,使用泛型来简化代码是最好的 (比如你想写个队列,写个链表、栈、堆之类的数据结构)

参考链接

  1. https://talkgo.org/t/topic/3582
  2. https://go.dev/doc/tutorial/generics
  3. https://segmentfault.com/a/1190000041634906
  4. https://go.dev/blog/go1.18
  5. https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md