注意: 本文主要是对 Go 夜读中 第 128 期视频 做的技术笔记摘抄

以及本文讨论范围仅限于 Go 1.18

什么是泛型

泛型编程的中心思想是对具体的、高效的算法进行抽象,以获得更通用的算法,然后这些算法可以与不同的数据表示方式结合起来,产生各种各样有用的软件

本质上就是: 将算法和类型解耦,以便更广泛地复用算法

为什么需要泛型

比如一个加法函数

func Add(a, b int32) int32 {
return a + b
}
int32intint64byteAddIntAddInt64AddByte

如果不用泛型,那么就会

interface {}

而使用泛型的最终目的,是为了 保证实参的传递是类型安全的,并且也能对实现的类型进行抽象

什么时候需要泛型

当使用 接口(interface) 作为函数的形参类型时,函数调用时传递的实参可以是 完全不同 的类型

当使用 类型参数(type parameter) 作为函数的形参类型时,函数调用时传递的实参 必须是满足类型参数所约束的类型

总之

slicemapchannel

Go 泛型是怎么样的

type parameter 长什么样

TCC

type set 长什么样

如图所示,类型集本质上就是接口,而对于接口来说,它也 可以具有类型参数

应用1:对实现的类型进行抽象

Map

Reduce

interface {}any

应用2:精简调用代码

func (s wrapSort[T]) ...sort.Sort

应用3:更多例子

类型参数的声明可以简化

如下图

但不推荐直接简化成

因为前者是对数组里面的元素做一个类型的抽象,而后者是对整个数组的整体做一个抽象,后者的意思就完全变了

一个困惑点

接口包含类型集时,无法作为具体的参数使用

比如

bar
barIa*T
barIb

抽象能力

Go 的泛型没有任何运行时(run-time)机制,所以关于泛型的组件就只发生在编译时(compile-time)

比如对于下面的接口,无法作为普通参数使用,因为它没有被实例化

类型参数的使用场合

  • 考虑使用的场合

    1. 函数的实现和参数类型 不强相关
    2. 要实现一个通用的数据结构时
    3. 能够扩大代码的使用范围和提升可读性时
  • 不考虑 使用的场合

    1. 一个方法的实现对不同类型都不相同时(有点废话)
    2. 可以用纯接口参数来代替类型参数时

比如下面的例子

core type

core type 即 核心类型

接口可以嵌入类型集,那么一个 约束 可以包含多种不同的类型

核心类型是指,一个接口只能约束一种底层类型(underlying type) 的情况,这个接口就是 核心类型

注意:普通类型的底层类型也属于核心类型

对于下面的接口来讲

他们的核心类型分别是

U*intVfloat32W

对类型使用运算符

==!=comparable

比如

<<=>>=constraints.Ordered

比如

但是在接口中 不能直接使用运算符方法

比如

主要原因可能是可比较性在本质上应该被定义为

所以下面这样的代码就会报错

x == yboolMyBool

而且比较对象的类型无法定义为自身类型

Go 泛型部分使用场景

返回满足接口约束的具体类型

比如下图

上面是一个获得返回不同类型的方法

要注意的一点的是,具有 指针接收器方法的类型必须使用指针类型作为类型参数

比如

func (b *B) Bar() {}
Want[B]()

BCBar*B

那么如何解决呢? 可以这么写

*TC

同时

UC

那么执行的结果就是

最终拿到的都是指针

类型参数的强制转换

int(x)T

无类型常量的类型推导

math.MaxFloat32float64

可能会破环现有的编程范式

比如下面的代码

ConfigWindow struct

WindowNewWindowOption


Config

[int, string]

现在的泛型还是会对代码产生一定程度的破坏

Go 泛型一些关键的算法

类型推断使用的方法是 类型合一(unification by substitution)

泛型的实例化通过字典和 Gcshape 模板

类型集的计算

Go 泛型的限制

comparable 的困境

下面代码会输出啥

Equal(v1, v2)

interface {}comparable
comparable

The type set of the comparable constraint is the set of all comparable types

interface {}comparable

注意: 在 Go 1.18 之前,接口类型的值是可以比较的

comparable

也导致了无法写出如下的代码(下图中 ERROR 部分)

[T any]anycomparable

对于上述情况,社区还在讨论中

不允许方法上的类型参数

目前还不允许这种写法(下图中的 ERROR 部分)

这主要是因为 Go 一个潜在的语言设计问题(运行时的类型断言)

xBar(int)xanyxtype Xx

类型集的本质

类型集的基本思想是设计一种能够表达一类类型的机制

设计这种机制的核心难点是保证不会出现 罗素悖论

例如不加以限制的 分类公理 是朴素集合论中的导致矛盾的根源

类型集可以从 ZF 系统来考虑

  • 外延公理: 两个类型集相等,当且仅当他们包含的类型相同
  • 分类公理: 给出一个类型集和一个普通接口,存在同时满足他们的子集
  • 并集公理: 两个类型集可以求并集
  • 空集公理: 存在一个不满足任何类型的类型集
  • 无穷公理: 存在一个包含无穷多个类型的类型集
  • 等等…

类型集的困境

检查类型是否满足接口描述的类型集,是一个典型的 可满足性问题(SAT)

这个问题是一个 NP 完全问题(Cook-Levi 定理)

编译器如果在编译期间执行这类检查,如果不对规则加以限制,则将在某些情况下极大的增加编译时间

ConstraintAConstraintB

所以 Go 1.18 中的类型集设计并不是完备的,即某些类型集无法这样表示

不支持变长类型参数

比如下图中写一个 可变长的元组类型就不行

更多限制

并同时不支持

  • 特化
  • 元编程
  • 柯里化
  • 非类型类型参数
  • 运算符方法

总结

  • 类型参数 更广泛的支持了基于接口的参数化多态
  • 类型集 使用了公理化集合论(更特别一点类型论)的方法扩展了接口的定义实现了类型约束
  • 类型推导类型合一 简化了泛型的使用

但是

Go 目前的泛型(1.18,但很可能在1.20之前都)还比较基础且限制较多(也不排除永远不会解除这些限制)
很多原因是权衡了编译器编译速度的结果(而非标新立异)

期待未来的改进空间

  1. 取消前面提到的限制
  2. 支持 parameterized aliases
  3. 编译器改善 big compile time improvements

参考链接

  • 《Tony Bai · Go 语言第一课》