Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

随着 Go 1.18 开始引入泛型到现在的 Go 1.21.3 版本,经过版本+时间跨度,泛型也有了一系列的改变,之前都是要用才看,版本迭代带来的变化首先最直观的就是接口的概念定义,由方法集转为类型集的定义,还有利用泛型在日常开发迭代中带来的便利性也是很高的,其中我封装的 Minimax API Warpper 中也用到了泛型,不过都是凤毛麟角,感觉泛型的加入让 Go 变得有点魔法的感觉hhhh。

泛型可以减少重复代码逻辑的工作,降低耦合度和提高抽象, 系统性的学一下泛型感觉还是很有必要的。

泛型定义

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

func AddFloat32(a, b float32) float32 {
    return a + b
}

func AddFloat64(a, b float64) float64 {
    return a + b
}

考虑上述计算和函数,我们设置了具体的类型形参 int, 它可以做到对整数的加法运算,这本身没有问题,但是如果我们想再写一个对 float32float64 做加法的函数,我们就不得不重新写一个新的函数,尽管它们的逻辑相同,却大大增加了冗余定义。

此时就可以利用泛型对这些相似逻辑的方法签名做一次抽象,把类型形参抽象为 T, 我们就可以实现一个函数的逻辑实现多种类型的函数定义:

// T int|float32|float64: 类型形参列表
// int|float32|float64: 类型约束
func Add[T int | float32 | float64 | string](a, b T) T {
    return a + b
}

// 调用示例
Add[string]("1", "2") // string 在类型约束集合中
Add[int](1, 2) // int 在类型约束集合中
Add(6.3, 6.4) // 简写形式,编译器会判断形参是否在类型约束集合中

// 更多定义
type MySlice [T int | string | float32 | float64] []T // 泛型切片
type MyMap map[KEY int | string, VALUE float32 | float64 | string] map[KEY]VALUE // 泛型 map
type MyStruct[T string] struct {
    Name T
    Age int
}

Go 以 [] 中括号确定了泛型定义,T 占位符后面紧跟的是适配的类型,只要在这个类型集合中的变量都可以作为该函数的类型形参,这相当于直接定义了一组集合来解决上面多冗余的函数签名带来的问题,其中 T int | float32 | float64 | string 也称为类型形参列表

类型形参的相互套用:

type MyStruct[T int | float32, S []T] struct {
    Tuples S
    MaxVal T
    MinVal T
}

类型形参列表可以引用多个占位符去表示不同的类型约束,也可以套用前有的类型约束,这使得泛型结构体异常灵活,同时也要注意初始化的类型形参问题,例如下面的初始化是不对的:

// 错误的初始化
_ := Mystruct[int, []float32]{
    Tuples: []float32{2.3, 1.1}
    MaxVal: 2
    MinVal: 1
}

一些错误的定义方式:

type MyType[T string | float32] T // 不能单独使用类型形参作为类型别名
type MyType[T *int | *float32] []T // 歧义
type MyType[T (int)] []T // 错误

type NewType[T int | string] int
var (
   a NewType[int] = 233 // 编译通过
   b NewType[string] = 233 // 编译通过
   c NewType[string] = "hello" // 编译失败,hello 不能赋值给底层类型 int
)

其中歧义的定义是因为编译器会 将T *int 误判为运算符而不是指针,这是一种修复方式,利用接口去包裹一下:

type MyType[T interface{*int | *string}] []T

泛型引入的同时也改变了接口的定义,从方法集 -> 类型集的转变,这个匿名接口的类型集合是 *int、*string 两种的并集

泛型方法

既然有了泛型定义,那也可以衍生出泛型定义的方法:泛型Receiver, 首先我们可以定义一个泛型类型,为它添加一个泛型方法

type MyType[T int | float32] []T

// 泛型方法
func (mt MyType[T]) Sum() T {
    var total T
    for _, v := range mt {
        total += v
    }

    return total
}

var mt MyType[int] = []int{1, 2, 3}
println(mt.Sum())

和普通方法不一样的是加多了一个类型约束符,有个泛型方法我们可以非常简单的创建通用的数据结构和一些通用的结构体,大大增强了维护的灵活性。

方法集和类型集

泛型引入后接口的定义有了全新的定义:接口从方法集(Method Set)转变为现在的类型集(Type Set)
当类型约束中的约束项不断增多时可能有如下定义:

type MySlice[T string | int | int32 | int64 | float32 | ...] []T

这种写法使得我们在维护这个泛型切片将是非常痛苦的一件事,所以可以将类型约束定义到接口中去维护

type Int interface {
    int | int32 | int8 | int64
}

type Uint interface {
    uint | uint8 | uint16 | uint32
}

type MyInterface interface {
    Int | Uint
}

type MySlice[T Int | Uint] []T
type MySlice[T MyInterface] []T

我们将一些具有相似特性的类型约束抽到接口层并暴露出来去作为自定义泛型类型的类型约束, 最后通过一个空接口将他们组合到一起

指定底层类型的类型约束:

type MyInt int
var s Slice[MyInt] // 编译错误

虽然MyInt的底层类型是int,但是实际上类型约束列表并没有指向底层类型的类型约束,因此可以采用~int的语法来标记该约束类型是可以包含指定底层类型为int的类型

type Int interface {
    ~int | ~int8 | ~int32 | ~int64
}

加上指定底层类型标识符的类型约束就可以初始化上面编译错误的结构体。但是不能将标识符作用于接口且类型必须为基本类型

type MyInt int

type _ interface {
    ~[]byte // 正确
    ~Myint // 错误,不是基本类型
    ~error // 错误,error 是接口
}

回到方法集和类型集本身,这种类型集的写法也就意味着的区别:

  • 方法集:只要某一类型实现了该方法集的所有方法,意味着这一类型实现了这个接口
  • 类型集:把接口看作是一个类型的集合,所有实现了类型的方法的类型都在接口代表的类型集合当中我们可以认为它实现了该接口(魔法起来了!!), 具体可以为以下两种条件:
    • T 不是接口时,类型 T 是接口 I 代表的类型集中的一个成员
    • T 是接口时, T 接口代表的类型集是 I 代表的类型集的子集

泛型类型集并集:

type AllInt interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

| 分割的类型为类型集并集

泛型类型集交集:

type AllInt interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint8
}

type Uint interface {
    ~uint | ~uint8 | ~uint16
}

type _ interface {
    AllInt
    Uint
}

type _ interface {
    AllInt
    ~int
}

以换行为标识符为类型集交集,我们对 AllIntUint 取交集,集合类型约束为 ~uint8

空集:

type _ interface {
    int
    float32 
}

内置泛型类型

当我们定义一些数据类型时需要类型为可比较类型或可排序类型,Go 专门提供了两个类型集分别是 comparable(可比较类型)和 ordered(可排序类型),他们的官方定义如下:

type comparable interface{ comparable }

type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

其中 comrarable 指的是可执行 != == 等运算符,并没确保这个类型可以执行大小比较

普通接口和泛型接口

普通接口:

type ReadWriter interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
}

只要实现该接口的所有方法即实现了该接口

泛型接口:

type ReadWriter interface {
    ~string, | ~[]rune

    Read([]byte) (int, error)
    Write([]byte) (int, error)
}

接口类型 ReadWriter 代表了一个类型集合,所有以 string 或 []rune 为底层类型,并且实现了 Read() Write() 这两个方法的类型都在 ReadWriter 代表的类型集当中。

// 类型 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) {
 ...
}

只有该类型是类型集合中类型约束的成员且实现了所有方法才算实现了泛型接口

用泛型实现的 SSE 协议的流式方法

这是前段时间封装 Minimax API Warpper 所用到泛型用例:

// 假设这是非流式的返回体
type ChatCompletionResponse struct {}

// 假设这是一个流式结果的返回体
type CompletionStreamResponse struct {
    *streamReader[ChatCompletionResponse]
}

// 定义一个类型集,里面只有一个类型成员
type streamAble interface {
    CompletionStreamResponse
}

// 定义一个泛型结构体,类型约束列表为 stramAble 类型集
type streamReader[T streamAble] struct {}

// 定义泛型方法: 接受流式消息
func (s *streamReader[T]) Recv() (T, error) {}

上述定义了一个简单的泛型类型集,可以返回符合类型约束的响应体,该响应体默认实现了该接口。