关于golang:Go-118-泛型全面讲解一篇讲清泛型的全部

11次阅读

共计 22923 个字符,预计需要花费 58 分钟才能阅读完成。

2022 年 3 月 15 日,争议十分大但同时也备受期待的泛型终于随同着 Go1.18 公布了。

可是因为 Go 对泛型的反对时间跨度太大,有十分多的以“泛型”为关键字的文章都是在介绍 Go1.18 之前的旧泛型提案或者设计。而很多设计最终在 Go1.18 中被废除或产生了更改。并且很多介绍 Go1.18 泛型的文章 (包含官网的) 都过于简略,并没对 Go 的泛型做残缺的介绍,也没让大家意识到这次 Go 引入泛型给语言减少了多少复杂度(当然也可能单纯是我没搜到更好的文章)

出于这些起因,我决定参考 The Go Programming Language Specification,写一篇比拟残缺零碎介绍 Go1.18 泛型的文章。这篇文章可能是目前介绍 Go 泛型比拟全面的文章之一了

💡 本文力求能让未接触过泛型编程的人也能较好了解 Go 的泛型,所以行文可能略显啰嗦。然而请置信我,看完这篇文章你能取得对 Go 泛型十分全面的理解

1. 所有从函数的形参和实参说起

假如咱们有个计算两数之和的函数

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

这个函数很简略,然而它有个问题——无奈计算 int 类型之外的和。如果咱们想计算浮点或者字符串的和该怎么办?解决办法之一就是像上面这样为不同类型定义不同的函数

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

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

可是除此之外还有没有更好的办法?答案是有的,咱们能够来回顾下函数的 形参 (parameter) 实参(argument) 这一基本概念:

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

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

咱们晓得,函数的 形参 (parameter) 只是相似占位符的货色并没有具体的值,只有咱们调用函数传入 实参(argument) 之后才有具体的值。

如果咱们将函数形参实参这个概念推广一下,给变量的类型引入和函数形参实参相似的概念的话,问题就迎刃而解:在这里咱们将其称之为 类型形参 (type parameter) 类型实参(type argumetn)

// 假如 T 是类型形参,在定义函数时它的类型是不确定的,相似占位符
func Add(a T, b T) T {return a + b}

在下面这段伪代码中,T 被称为 类型形参,它不是具体的类型,在定义函数时类型并不确定。因为 T 的类型并不确定,所以咱们能够像函数的形参那样,在调用函数的时候再传入具体的类型。这样咱们不就能一个函数同时反对多个不同的类型了吗?

就像上面的伪代码一样:

// [T=int]中的 int 是类型实参,代表着函数 Add()定义中的类型形参 T 全都被 int 替换
Add[T=int](100, 200)  
// 传入类型实参 int 后,Add()函数的定义可近似看成上面这样:func Add(a int, b int) int {return a + b}

// 另一个例子,[T=string]中的 string 是类型实参
Add[T=string]("Hello", "World") 
// 类型实参 string 传入后,Add()函数的开一能够近似是为上面这样
func Add(a string, b string) string {return a + b}

通过下面这样引入了 类型形参 类型实参 后,咱们就让一个函数取得了解决多个不同类型的能力,咱们称为 泛型编程

可能你会已奇怪,这种类型动静解决类型的性能,我通过 Go 的接口和反射仿佛也能实现?泛型能比接口 + 反射更加轻松高性能地实现很多性能,但自身也有很多限度。至于该抉择泛型还是接口 + 反射,记住上面这样的一条规定:

如果你常常要别离为不同的类型写齐全同样逻辑的代码,那么应用泛型将是最合适的抉择

2. Go 的泛型

通过下面的内容,咱们实际上曾经对 Go 的泛型编程有了最初步也是最重要的意识—— 类型形参 和 类型实参。而 Go1.18 也是通过这种形式实现的泛型,然而单纯的形参实参是远远不能实现泛型编程的,所以 Go 还引入了十分多全新的概念:

  • 类型形参 (Type parameter)
  • 类型实参(Type argument)
  • 类型形参列表(Type parameter list)
  • 类型束缚(Type constraint)
  • 实例化(Instantiations)
  • 泛型类型(Generic type)
  • 泛型接收器(Generic receiver)
  • 泛型函数(Generic function)

等等等等。

啊,切实概念太多了头晕?没事请跟着我慢慢来,首先从 泛型类型(generic type) 讲起

3. 类型形参、类型实参、类型束缚和泛型类型

察看上面这个简略的例子:

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 这部分被称为 类型束缚(Type constraint),两头的 | 的意思是通知编译器,类型形参 T 能够接管 int 或 float32 这两种类型
  • 中括号里的 T int|float32 这一串因为定义了所有的类型形参 (在这个例子里只有一个类型形参),所以咱们称其为 类型形参列表(type parameter list)
  • 这里新定义的类型名称叫 Slice[T]

很显著,这种类型定义的形式中带了类型形参,和一般的类型定义十分不一样,所以咱们将这种

类型定义中带 类型形参 的类型,称之为 泛型类型(Generic type)

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

// ✗ 谬误。Slice[T]是泛型类型,不可间接应用必须实例化
var x Slice[T] = []int{1, 2, 3} 

// ✓ 正确。这里传入了类型实参 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 中,不能用来实例化泛型类型
var c Slice[string] = []string{"Hello", "World"} 

在下面的例子中,咱们首先通过给泛型类型 Slice[T] 传入了类型实参 int,将其实例化为了具体的类型 Slice[int]。这时候咱们就能够把它的类型定义视为 type Slice[int] []int。其中实例化后的类型名为 Slice[int],其底层类型是 []int。前面传入 float32 实例化同理。

并且因为通过实例化之后,变量 a 和 b 就是具体的不同类型了(一个 Slice[int],一个 Slice[float32]),所以 a = b 这样不同类型之间的变量赋值是不容许的。

同时,因为 Slice[T] 的类型束缚限定了只能应用 int 和 float32 来实例化本人,所以所以 Slice[string] 这样应用 string 类型来实例化是谬误的。

下面只是个最简略的例子,实际上类型形参能够远远不止一个,并且也能够应用在任何类型的定义之中,如下

// MyMap 类型定义了两个类型形参 KEY 和 VALUE。别离为两个形参制订了不同的类型束缚
type MyMap[KEY int | string, VALUE float32 | float64] map[KEY]VALUE  

// 别离用类型实参 string flaot64 替换了类型形参 KEY 和 VALUE 来实例化泛型类型 MyMap[KEY, VALUE]
var a MyMap[string, float64] = map[string]float64{
    "jack_score": 9.6,
    "bob_score":  8.4,
}

用下面的例子从新温习下各种概念的话:

  • KEY 和 VALUE 是 类型形参
  • int|string 是 KEY 的 类型束缚 float32|float64 是类型 VALUE 的 类型束缚
  • KEY int|string, VALUE float32|float64 整个一串文本被称为 类型形参列表
  • Map[KEY, VALUE] 是 泛型类型,类型名称为 Map[KEY, VALUE]
  • var a MyMap[string, float64] = xx 中的 string 和 float64 是 类型实参 ,用于别离替换 KEY 和 VALUE, 实例化 出了具体的类型 MyMap[string, float64]

还有点头晕?没事,确实一下子有太多概念了,这里用一张图就能简略说分明:

Go 泛型概念一览

3.1 其余的泛型类型

除此之外还有诸如构造体以及接口之类的定义也能应用类型形参:

// 一个泛型类型的构造体。可用 int 或 sring 类型实例化
type MyStruct[T int | string] struct {  
    Name string
    Data T
}

// 一个泛型接口
type PrintData[T int | float32 | string] interface {Print(data T)
}

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

3.2 类型形参的相互套用

在类型形参列表中的类型形参是能够相互套用的,如下

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

这个例子看起来有点简单且难以了解,但实际上只有记住一点,任何泛型类型都必须传入类型实参实例化才能够应用就容易了解了。咱们这就尝试传入下类型实参看看就:

ws := WowStruct[int, []int]{Data:     []int{1, 2, 3},
        MaxValue: 3,
        MinValue: 1,
    }

在这个例子中,类型形参的定义是 []T,而咱们给 T 传入了类型实参 int,所以 S 就应该传入类型实参 []int。如果像上面这样的话则是谬误的:

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

传入类型实参后,泛型类型 WowStuct[T, S] 被实例化,生成了一个新的具体的类型 WowStruct[int, []int],这个类型的定义可近似视为如下:

type WowStruct[int, []int] struct {Data     []iont
    MaxValue int
    MinValue int
}

3.3 几种语法错误

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

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

    //✗ 谬误。T *int 会被编译器误认为是表达式(T 乘以 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{}解决问题

3.4 非凡的泛型类型

这里探讨种比拟非凡的泛型类型,如下:

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

这个例子没有什么具体意义,然而能够让我了解泛型类型的实例化的机制

3.5 泛型类型的套娃

泛型和一般的类型一样,能够相互嵌套定义出更加简单的新类型,如下:

type Slice[T int|string|float32|float64] []T

// ✗ 谬误。泛型类型 Slice 的类型束缚中不蕴含 uint, uint8
type UintSlice[T uint|uint8] Slice[T]  

// ✓ 正确。基于泛型类型 Slice 定义了新的泛型类型 FloatSlice。FloatSlice 只承受 float32 和 float64 两种类型
type FloatSlice[T float32|float64] Slice[T] 

// ✓ 正确。基于泛型类型 Slice 定义的新泛型类型
type IntAndStringSlice[T int|string] Slice[T]  
// ✓ 也正确 基于 IntAndStringSlice 定义出的新泛型类型
type IntSlice[T int] IntAndStringSlice[T] 

// 在 map 中套一个泛型类型 Slice[T]
type WowMap[T int|string] map[string]Slice[T]
// 在 map 中套 Slice 的另一种写法
type WowMap2[T Slice[int] | Slice[string]] map[string]T

3.6 泛型束缚的两种抉择

察看上面两种类型束缚的写法

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
}

3.7 匿名构造体不反对泛型

咱们有时候会常常应用到匿名的构造体 (struct) 并在定义之后间接初始化匿名构造体,如下:

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

解决办法就是用泛型的时候给构造体命名,不必匿名构造,。对于很多场景的应用来说的确比拟麻烦(最次要麻烦集中在单元测试的时候,对泛型函数之类的做单元测试会变得十分麻烦,这点我之后的文章将会具体论述)

4. 泛型 receiver

看了上的例子,你肯定会说,介绍了这么多简单的概念,但如同泛型类型基本没什么用途啊?

是的,单纯的泛型类型实际上对开发来说用途并不大。然而如果将泛型类型和接下来要介绍的泛型 receiver 相结合的话,泛型就有了十分大的实用性了

咱们晓得,定义了新的一般类型之后能够给类型增加办法。那么能够给泛型类型增加办法吗?答案天然是能够的,如下:

type MySlice[T int | float32] []T

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

这个例子为泛型类型 MySlice[T] 增加了一个计算成员总和的办法 Sum()。留神察看这个办法的定义:

  • 首先看 receiver (s MySlice[T]),因为下面这种泛型类型的名称叫 MySlice[T],所以咱们间接把类型名写入了 receiverr 中
  • 而后办法的返回参数咱们也应用了 类型形参(实际上如果有需要的话,办法的接管参数也能够实用类型形参)
  • 在办法的定义中,咱们也能够实用类型形参 T,这里咱们定义了一个新的变量 sum : var sum T

对于这个泛型类型 MySlice[T] 咱们该如何应用?还记不记得之前强调过很屡次的,泛型类型无论如何都须要先用类型实参实例化,所以用法如下:

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,最终咱们能够把代码看作上面这样这样:

type MySlice[int] []int // 实例化后的类型名叫 MyIntSlice[int]

// 办法 中所有类型形参 T 被替换为类型实参
func (s MySlice[int]) Sum() int {
    var sum int 
    for _, value := range s {sum += value}
    return sum
}

用 float32 实例化和用 int 实例化同理,此处不再赘述。

通过泛型 receiver,泛型的实用性一下子失去了微小的扩大。在没有泛型之前,如果想实现诸如堆,栈、队列、链表之类的数据结构,咱们要么

  1. 为每种类型写一个实现
  2. 应用 interface{} 接口

而有了泛型之后,咱们就能非常简单地创立通用地数据结构构造了。接下来用一个更加实用地例子——队列来解说

4.1 基于泛型的队列

队列是一种先入先出的数据结构,它和事实中排队一样,数据只能从队尾部放入和从队首取出,先放入的数据优先被取出来

// 这里类型束缚应用了空接口,代表的意思是所有类型都能够用来实例化泛型类型 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, false
}

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

💡 为了不便阐明,下面是队列非常简单的一种实现办法,没有思考线程平安等很多问题

首先察看构造体的类型形参列表 T interface{},类型束缚应用了一个空接口,当类型束缚应用空接口的时候并不代表这个泛型类型只能像上面这样应用空接口实例化:var q Queue[interface{}],而是所有类型都可用来实例化(对于接口相干地具体阐明参考后半局部阐明)

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] // 可寄存接口的队列
// ......

4.2 动静判断变量的类型

应用接口的时候常常会用到类型断言或 type swith 来确定接口具体的类型,而后对不同类型做出不同的解决,如:

var i interface{} = 123
i.(int) // 类型断言

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

那么你肯定会想到,对于 valut T 这样通过类型形参定义的变量,咱们能不能判断具体类型而后对不同类型做出不同解决呢?答案是不容许的,如下:

func (q *Queue[T]) Put(value T) {
    // 谬误。不容许应用 type switch 来判断 value 的具体类型
    switch T.(type) {
    case int:
        // do something
    case string:
        // do something
    default:
        // do something
    }
    ...
}

尽管 type switch 不能,可通过反射机制咱们就能曲线救国实现对应的性能:

func (receiver Queue[T]) Put(value T) {// Printf() 可输入变量 value 的类型(底层就是通过反射实现的)
    fmt.Printf("%T", value) 

  // 通过反射能够动静取得变量 value 类型从而分状况解决
    valueType := reflect.ValueOf(value)

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

    ...
}

这看起来达到了咱们的目标,可是当你写出下面这样的代码时候就呈现了一个问题:

你为了防止应用反射而抉择了泛型,后果到头来又为了一些性能在在泛型中应用反射。当呈现这种状况的时候你可能须要从新思考一下,本人的需要是不是真的须要用泛型(毕竟泛型机制自身就很简单了,再加上反射的复杂度,减少的复杂度并不一定值得)

当然,这所有选择权都在你本人的手里,依据具体情况斟酌

5. 泛型函数

在介绍完泛型类型和泛型 receiver 之后,咱们来介绍最初一个能够应用泛型的中央——泛型函数。有了下面的常识,写泛型函数也非常简略。假如咱们想要写一个计算两个数之和的函数:

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

这个函数天经地义只能计算 int 的和,而浮点的计算是不反对的。这时候咱们能够像上面这样定义一个泛型函数:

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

Add[string]("hello", "world") // 谬误。因为泛型函数 Add 的类型束缚中并不蕴含 string

或者你会感觉这样每次都要手动指定类型实参太不不便了。所以 Go 还反对类型实参的主动推导:

Add(1, 2)  // 1,2 是 int 类型,编译请主动推导出类型实参 T 是 int
Add(1.0, 2.0) // 1.0, 2.0 是浮点,编译请主动推导出类型实参 T 是 float32

主动推导的写法就如同免去了传入实参的步骤一样,但请记住这仅仅只是编译器帮咱们推导出了类型实参,实际上传入实参步骤还是产生了的。

5.1 匿名函数不反对泛型

在 Go 中咱们常常会应用匿名函数,如:

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

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

那么 Go 支不反对匿名泛型函数呢?答案是不能——匿名函数签名中不能蕴含类型形参:

// 谬误,不反对匿名泛型函数
fnGeneric := func[T int | float32](a, b T) T {return a + b} 

fmt.Println(fnGeneric(1, 2))

然而在匿名函数中应用类型形参是能够:

func MyFunc[T int | string](a, b T) {fn := func() {
        var c T     // 匿名函数可应用类型形参
        c = a + b
        fmt.Println(c)
    }

    fn()}

5.2 既然反对泛型函数,那么泛型办法呢?

既然函数都反对了泛型了,那你应该天然会想到,办法支不反对泛型?很可怜,目前 Go 的办法并不反对泛型,如下:

type A struct {
}

// 不反对泛型办法
func (receiver A) Add[T int | float32 | float64](a T, b T) T {return a + b}

然而因为 receiver 反对泛型,所以如果想在办法中应用泛型的话,目前惟一的方法就是曲线救国,曲折地在类型中定义形参:

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)

前半小结

讲完了泛型类型、泛型 receiver、泛型函数后,Go 的泛型算是介绍完一半多了。在这里咱们做一个概念的小结:

  1. Go 的泛型目前可应用在 3 个中央

    1. 泛型类型 – 类型定义中带类型形参的类型
    2. 泛型 receiver – 泛型类型的 receiver
    3. 泛型函数 – 带类型形参的函数
  2. 为了实现泛型,Go 引入了一些新的概念:

    1. 类型形参
    2. 类型形参列表
    3. 类型实参
    4. 类型束缚
    5. 实例化 – 泛型类型不能间接应用,要应用的话必须传入类型实参进行实例化

什么,这文章曾经很长很简单了,才讲了一半?是的,Go 这次 1.18 引入泛型为语言减少了较大的复杂度,目前还只是新概念的介绍,上面后半段将介绍 Go 引入泛型后对接口做出的重大调整。那么做好心理准备,咱们登程吧。

6. 变得复杂的接口

有时候应用泛型编程时,咱们会书写长长的类型束缚,如下:

// 一个能够包容所有 int,uint 以及浮点类型切片的泛型类型
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T

天经地义,这种写法是咱们无法忍受也难以保护的,而 Go 反对将类型束缚独自拿进去定义到接口中,从而让代码更容易保护:

type IntUintFloat interface {int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64}

type Slice[T IntUintFloat] []T

这段代码把类型束缚给独自拿进去,写入了接口类型 IntUintFloat 当中。须要指定类型束缚的时候间接应用接口 IntUintFloat 即可。

不过这样的代码仍旧不好保护,而接口和接口、接口和一般类型之间也是能够通过 | 进行组合:

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  // 应用 '|' 将多个接口类型组合

下面的代码中,咱们别离定义了 Int, Uint, Float 三个接口类型,并最终在 Slice[T] 的类型束缚中中通过应用 | 将它们组合到一起。

同时,接口也能组合其余接口,所以还能够像上面这样:

type SliceElement interface {Int | Uint | Float | string // 组合了三个接口类型并额定减少了一个 string 类型}

type Slice[T SliceElement] []T 

6.1 ~ : 指定底层类型

下面定义的 Slie[T]尽管能够达到目标,然而有一个毛病:

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 为底层类型的类型也都可用于实例化。

应用 ~ 对代码进行改写之后如下:

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. ~ 前面的类型必须为底层类型
type MyInt int

type _ interface {~[]byte  // 正确
    ~MyInt   // 谬误,~ 后的类型必须为底层类型
    ~error   // 谬误,~ 后的类型不能为接口
}

6.2 从办法集 (Method set) 到类型集(Type set)

下面的例子中,咱们学习到了一种接口的全新写法,而这种写法在 Go1.18 之前是不存在的。如果你比拟敏锐的话,肯定会隐约意识到这种写法的扭转这也肯定意味着 Go 语言中 接口(interface{}) 这个概念产生了十分大的变动。

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

An interface type specifies a method set called its interface

就如上面这个代码一样,ReadWriter 接口定义了一个接口(办法集),这个汇合中蕴含了 Read()Write() 这两个办法。所有同时定义了这两种办法的类型被视为实现了这一接口。

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
(*一种接口类型定义了一个类型集)

你或者会感觉,这不就是改了下概念上的定义实际上没什么用吗?是的,如果接口性能没变动的话的确如此。然而还记得上面这种用接口来简化类型束缚的写法吗:

type Float interface {~float32 | ~float64}

type Slice[T Float] []T 

这就体现出了为什么要更改接口的定义了。用 办法集 的概念从新了解下下面的代码:

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

而泛型类型 Slice[T] 的 类型束缚 的真正意思是:类型束缚指定了对应类型形参可用的类型汇合,只有属于这个汇合中的类型能力替换形参用于泛型类型的实例化,如:

var s Slice[int]          // int 类型属于 T 的类型束缚限定的类型集,所以 int 能够作为类型实参
var s Slice[chan int] // chan int 类型不在 T 的类型束缚限定的类型集中,所以谬误

6.2.1 接口实现 (implement) 定义的变动

既然接口定义产生了变动,那么从 Go1.18 开始 接口实现(implement) 的定义天然也产生了变动:

当满足以下条件时,咱们能够说 类型 T 实现了接口 I (type T implements interface I)

  • T 不是接口时:类型 T 时接口代表的类型集中的一个成员 (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)

6.2.2 类型的并集

并集咱们曾经很相熟了,之前始终应用的 | 符号就是求类型的并集(union )

type Uint interface {  // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

6.2.3 类型的交加

接口能够不止书写一行,如果一个接口有多行类型定义,那么取它们之间的 交加

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 代表的是 AllInt 与 Uint 的 交加,即 ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
  • 接口 B 代表的则是 AllInt 和 ~int 的 交加

下面的代码等价于如下:

type A interface {~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64}

type B interface {~int}

除了下面的交加,上面也是一种交加:

type C interface {
    ~int
    int
}

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

6.2.4 空集

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

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

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

6.2.5 空接口和 any

下面说了空集,接下来说一个非凡的汇合,空接口 interface{}。因为,Go1.18 开始接口的定义产生了扭转,所以 interface{} 的定义也产生了一些变更:

空接口代表所有类型的汇合

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

  1. 尽管空接口内没有写入任何的类型,但它代表的是所有类型的汇合,而非一个 空集
  2. 类型束缚中指定 空接口 的意思是指定了一个蕴含所有类型的类型集,并不是类型束缚限定了只能应用 空接口 来做类型形参

    // 空接口代表所有类型的汇合。写入类型束缚意味着所有类型都可拿来做类型实参
    type Slice[T interface{}] []T
    
    var s1 Slice[int] []T    // 正确
    var s2 Slice[map[string]string] T  // 正确
    var s3 Slice[chan int]  // 正确
    var s4 Slice[interface{}]  // 正确

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

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

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

// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{} 

所以从 go 1.18 开始,所有能够用到空接口的中央其实都能够间接替换为 any。如:

var s []any // 等价于 var s []interface{}
var m map[string]any // 等价于 var m map[string]interface{}

func MyPrint(value any){fmt.Println(value)
}

如果你快乐得话,我的项目迁徙到 1.18 之后能够应用上面这行命令间接把整个我的项目中的空接口全都替换成 any。当然因为并不强制,所以到底是用 interface{} 还是 any 全看本人爱好

gofmt -w -r 'interface{} -> any' ./...

💡 Go 语言我的项目中就已经有人提出过把 Go 语言中所有 interface{}替换成 any 的 issue,而后因为影响范畴过大过而且影响因素不确定,天经地义被驳回了

6.2.6 comparable(可比拟) 和 可排序(ordered)

对于一些数据类型,咱们须要在类型束缚中限度只能承受可用 !=== 比照的类型,如 map:

// 谬误。因为 map 中键的类型必须是可进行 != 和 == 比拟的类型
type MyMap[KEY any, VALUE any] map[KEY]VALUE 

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

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

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

type OhMyStruct struct {a int}

var a, b OhMyStruct

a == b // 正确。构造体可应用 == 进行比拟
a != b // 正确

a > b // 谬误。构造体不可比大小

而可进行大小比拟的类型被称为 Orderd。目前 Go 语言并没有像 comparable 这样间接内置对应的关键词,所以想要的话须要本人来定义相干接口,比方咱们能够参考 Go 官网包golang.org/x/exp/constraints 如何定义:

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 包,今后可能会产生十分大变动,所以并不举荐间接应用

6.3 接口两种类型

咱们接下来再察看一个例子,这个例子是论述接口类型集概念最好的例子:

type ReadWriter interface {~string | | ~[]byte

    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

最开始看到这一例子你肯定有点懵不太了解它代表的意思,然而没关系,咱们用类型集的概念就能比拟轻松了解这个接口的意思:

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

如上面代码中,StringReadWriter 属于接口 ReadWriter 代表的类型集中,而因为 BytesReadWriter 的底层类型是 []byte,所以它不属于 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) {...}

你肯定会说,啊等等,这接口也变得太简单了把,那我定义一个 ReadWriter 接口,而后赋值的时候不光要思考到办法的实现,还必须思考到具体底层类型?心智累赘也太大了吧。是的,为了解决这个问题也为了放弃 Go 语言的兼容性,Go1.18 开始将接口分为了两种类型

  • 根本接口(Basic interface)
  • 个别接口(General interface)

6.3.1 根本接口(Basic interface)

接口定义中如果只有办法的话,那么这种接口被称为 根本接口(Basic interface)。这种写法就是 Go1.18 之前接口,其用法也根本和 Go1.18 之前保持一致。根本接口能够用于如下几个中央:

  • 最罕用的,定义接口变量

    type MyError interface { // 接口中只有办法,所以是根本接口
        Error() string}
    
    var err MyError = fmt.Errorf("hello world")
  • 根本接口因为也代表了一个类型集,所以可用在类型束缚中

    type MySlice[T io.Reader | io.Writer]  []Slice

6.3.2 个别接口(General interface)

如果接口内不光只有办法,还有类型的话,这种接口被称为 个别接口(General interface),如下例子都是个别接口:

type Uint interface { // 接口 Uint 带类型所以是个别接口
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type ReadWriter interface {  // 接口带办法也带类型,所以是个别接口
    ~string | | ~[]byte

    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

个别接口不能用于变量定义和赋值,只能用于泛型的类型束缚中。所以以下的用法是谬误的:

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

var uintInf Uint // 谬误。Uint 是个别接口,只能用于类型束缚,不得用于变量定义

这一限度保障了个别接口的应用被限定在了泛型之中,不会影响到 Go1.18 之前的代码,同时也极大缩小了书写代码时的心智累赘

6.4 泛型接口

接口也能够应用类型形参,察看上面这两个例子:

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):

DataProcessor[string]

// 实例化之后的接口定义相当于如下所示:type DataProcessor[string] interface {Process(oriData string) (newData string)
    Save(data string) error
}

通过实例化之后就好了解了,DataProcessor[string] 因为只有办法,所以它实际上就是个 根本接口(Basic interface),这个接口蕴含两个能解决 string 类型的办法。只有像上面这样实现了这两个能解决 string 类型的办法才算实现了这个接口:

type CSVProcessor struct {
}

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]

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. 个别接口不能用于变量定义只能用于类型束缚,所以接口 DataProcessor2[string] 只是定义了一个用于类型束缚的类型集
// XMLProcessor 未实现 DataProcessor2[string],因为它的底层类型是 []byte
type XMLProcessor []byte

func (c XMLProcessor) Process(oriData string) (newData string) {

}

func (c XMLProcessor) Save(oriData string) error {

}

// JsonProcessor 实现了接口 DataProcessor2[string],因为它底层类型是 struct{Data interface{} }
type JsonProcessor struct {Data interface{}
}

func (c JsonProcessor) Process(oriData string) (newData string) {

}

func (c JsonProcessor) Save(oriData string) error {

}

// 谬误。尽管 JsonProcessor 实现了 DataProcessor2[string]接口,但 DataProcessor2[string]是个别接口不能用于创立变量
var processor DataProcessor2[string] = JsonProcessor{} 

// 正确,实例化之后的 DataProcessor2[string] 可用于泛型的类型束缚
type ProcessorList[T DataProcessor2[string]] []T

// 正确,接口能够并入其余接口
type StringProcessor interface {DataProcessor2[string]

    PrintString()}

// 谬误,带办法的个别接口不能作为类型并集的成员(参考
type StringProcessor interface {DataProcessor2[string] | DataProcessor2[[]byte]

    PrintString()}

6.5 接口定义的种种限度规定

Go1.18 从开始,在定义类型集 (接口) 的时候减少了十分多非常琐碎的限度规定,因为找不到好的中央介绍,所以在这里对立介绍下:

  1. | 连贯多个类型的时候,类型之间不能有相交的局部(即必须是不交汇合):

    type MyInt int
    
    // 谬误,MyInt 的底层类型是 int, 和 ~int 有相交的局部
    type _ interface {~int | MyInt}

    然而相交的类型中有接口的话,则不受这一限度:

    type MyInt int
    
    type _ interface {~int | interface{ MyInt}  // 正确
    }
  2. 类型的并集中不能有类型形参

    type MyInf[T ~int | ~string] interface {~float32 | T  // 谬误。T 是类型形参}
    
    type MyInf2[T ~int | ~string] interface {T  // 谬误}
  3. 接口不能间接间接地并入本人(即使是通过类型集也不行)

    type Bad interface {Bad // 谬误,接口不能间接并入本人}
    
    type Bad2 interface {Bad1}
    type Bad1 interface {Bad2 // 谬误,接口 Bad1 通过 Bad2 间接并入了本人}
    
    type Bad3 interface {~int | ~string | Bad3 // 谬误,通过类型汇合类并入了本人}
    
  4. 类型并集大于一个类型的时候,不能间接或间接蕴含预约义的 comparable 接口,也不能间接或间接蕴含有带办法的接口

    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}  // 天经地义这样也是不行的
    }
    
    type InfWithMethod interface {
        ~string
        Hello()}
    type Bad4 interface {int | InfWithMethod // 谬误,类型并集并入了带办法的接口}
    
    type OK2 interface {InfWithMethod // 正确,这里是间接内嵌了 InfWithMethod 接口}
    type Bad5 interface {~int | Bad5  // 谬误,类型并集中间接并入带办法的接口也是也不行}
  5. 带办法的接口(无论是根本接口还是个别接口),都不能写入接口的类型并集中:

    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] 
    }
    

7. 总结

至此,终于是从头到位把 Go1.18 的泛型给介绍结束了。因为 Go 这次引入泛型带入了挺大的复杂度,也减少了挺多比拟零散琐碎的规定限度。所以写这篇文章断断续续花了我差不多一星期工夫。泛型尽管很受期待,但实际上举荐的应用场景也并没有那么宽泛,对于泛型的应用,咱们应该恪守上面的规定:

泛型并不取代 Go1.18 之前用接口实现的动静类型,在上面情景的时候非常适合应用泛型:当你须要针对不同类型书写同样逻辑的代码的时候,应用泛型来简化代码是最好的(如你想写个队列

参考资料

  • The Go Programming Language Specification – The Go Programming Language
正文完
 0