go中的泛型

https://segmentfault.com/a/1190000041634906

了解概念

  • 类型形参(Type parameter)
  • 类型实参(Type argument)
  • 类型形参列表(Type parameter list)
  • 类型约束(Type constraint)
  • 实例化(Instantiations)
  • 泛型类型(Generic type)
  • 泛型接收器(Generic receiver)
  • 泛型函数(Generic function)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type MySlice[T int | string] []T

type MyMap[K string | int, V int | float32 | float64] map[K]V

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

type IPrintData[T int | float32 | string] interface {
Print(data T)
}

type MyChan[T int | string] chan T

// 类型形参可以互相套用
type WowStruct[T int | float32, S []T] struct {
Data S
MaxValue T
MinValue T
}

任何泛型类型都必须传入类型实参实例化才可以使用:

1
m := MyMap[string, int]{"a": 1, "b": 2}

注意:匿名结构体不支持泛型

几种常见错误

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

1
type CommonType[T int|string|float32] T   // 错误

2、当类型约束的一些写法会被编译器误认为是表达式时会报错。如下:

1
2
type NewType[T *int] []T    // 错误。T *int会被编译器误认为是表达式 T乘以int,而不是int指针
type NewType2[T *int|*float64] []T // 错误。和上面一样,这里不光*被会认为是乘号,| 还会被认为是按位或操作

为了避免这种误解,解决办法就是给类型约束包上 interface{} 或加上逗号消除歧义

1
2
type NewType[T interface{*int}] []T
type NewType2[T interface{*int|*float64}] []T

泛型 receiver

单纯的泛型类型实际上对开发来说用处并不大,但泛型类型和泛型 receiver 相结合,就有了非常大的实用性。

我们知道,定义了新的普通类型之后可以给类型添加方法。那么可以给泛型类型添加方法吗?答案自然是可以的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type MySlice[T int | float32] []T

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

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

动态判断变量类型

使用接口的时候经常会用到类型断言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 这样通过类型形参定义的变量,不能使用类型断言type swith 判断具体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (q *Queue[T]) Put(value T) {
value.(int) // 错误。泛型类型定义的变量不能使用类型断言

// 错误。不允许使用type switch 来判断 value 的具体类型
switch value.(type) {
case int:
// do something
case string:
// do something
default:
// do something
}

// ...
}

可以通过反射机制达到目的:

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
6
7
8
9
10
func Add[T int | float32 | float64](a T, b T) T {
return a + b
}

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

// Go还支持类型实参的自动推导
Add(1, 2) // 1,2是int类型,编译请自动推导出类型实参T是int
Add(1.0, 2.0) // 1.0, 2.0 是浮点,编译请自动推导出类型实参T是float32

注意:

  1. 匿名函数不支持泛型
  2. 虽然函数支持泛型,但是方法不支持泛型
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 间接实现:

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

这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat 当中。需要指定类型约束的时候直接使用接口 IntUintFloat 即可。

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

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

~ 指定底层类型

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]

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

  • ~后面的类型不能为接口
  • ~后面的类型必须为基本类型

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

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

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

从 Go1.18 开始,接口的定义正式更改为了 类型集(Type set)。

接口实现(implement)定义的变化

既然接口定义发生了变化,那么从 Go1.18 开始 接口实现(implement) 的定义自然也发生了变化。

当满足以下条件时,我们可以说 类型 T 实现了接口 I

  • T 不是接口时:类型 T 是接口 I 代表的类型集中的一个成员
  • T 是接口时: T 接口代表的类型集是 I 代表的类型集的子集

类型的交集

如果一个接口有多个类型的定义,取它们之间的 交集

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

上面的例子中:

  • 接口A代表的是AllIntUint交集,即:~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
  • 接口B代表的是AllInt~int的交集,即:~int

interface{}any

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

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

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

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

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

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

注意:可比较指的是可以执行!=== 操作,并没有确保这个类型可以执行大小比较(> < >= <=

而可进行大小比较的类型被称为 Orderd 。目前 Go 并没有像 comparable 这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考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 代表的类型集当中。

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之前保持一致
    err := MyError
  • 用在类型约束中

    1
    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 any }

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

接口定义的种种限制规则

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