Go 博客

告别核心类型 - 拥抱我们所熟知和喜爱的 Go!

Robert Griesemer
2025 年 3 月 26 日

Go 1.18 版本引入了泛型,随之带来了一些新功能,包括类型参数、类型约束以及类型集等新概念。它还引入了“核心类型”的概念。虽然前者提供了具体的全新功能,但核心类型是一个抽象构造,是为了方便和简化处理泛型操作数(类型为类型参数的操作数)而引入的。在 Go 编译器中,过去依赖操作数底层类型的代码,现在必须调用一个计算操作数核心类型的函数。在语言规范中,很多地方我们只需要将“底层类型”替换为“核心类型”。这有什么不好的呢?

事实证明,有很多不好的地方!要理解我们为什么会走到这一步,回顾一下类型参数和类型约束的工作原理会很有用。

类型参数和类型约束

类型参数是未来类型实参的占位符;它像一个类型变量,其值在编译时已知,类似于命名常量代表在编译时已知的数字、字符串或布尔值。与普通变量一样,类型参数也有一个类型。该类型由其类型约束描述,类型约束决定了对类型为相应类型参数的操作数允许哪些操作。

实例化类型参数的任何具体类型都必须满足类型参数的约束。这确保了类型为类型参数的操作数拥有相应类型约束的所有属性,无论使用何种具体类型来实例化类型参数。

在 Go 中,类型约束通过方法和类型要求的混合来描述,它们共同定义了一个类型集:这是满足所有要求的所有类型的集合。Go 为此目的使用了一种广义的接口形式。接口枚举了一组方法和类型,此类接口描述的类型集由实现这些方法并包含在枚举类型中的所有类型组成。

例如,接口描述的类型集

type Constraint interface {
    ~[]byte | ~string
    Hash() uint64
}

由所有表示形式为[]bytestring且其方法集包含Hash方法的类型组成。

有了这些,我们现在可以写下管理泛型操作数操作的规则。例如,索引表达式的规则规定(除其他外)对于类型参数类型P的操作数a

索引表达式a[x]必须对P类型集中的所有类型的值都有效。P类型集中的所有类型的元素类型必须相同。(在此上下文中,字符串类型的元素类型是byte。)

这些规则使得索引下面的泛型变量s成为可能(playground

func at[bytestring Constraint](s bytestring, i int) byte {
    return s[i]
}

允许索引操作s[i],因为s的类型是bytestring,并且bytestring的类型约束(类型集)包含[]bytestring类型,对于它们来说,用i索引是有效的。

核心类型

这种基于类型集的方法非常灵活,并且符合最初的泛型提案的意图:涉及泛型类型操作数的操作应该是有效的,如果它对相应类型约束允许的任何类型都有效。为了简化实现方面的问题,知道我们以后能够放宽规则,这种方法并没有被普遍选择。相反,例如,对于发送语句,规范规定

通道表达式的核心类型必须是通道,通道方向必须允许发送操作,并且要发送的值的类型必须可以赋值给通道的元素类型。

这些规则基于核心类型的概念,其定义大致如下

  • 如果一个类型不是类型参数,它的核心类型就是它的底层类型
  • 如果该类型是类型参数,则核心类型是类型参数类型集中所有类型的单一底层类型。如果类型集具有不同的底层类型,则核心类型不存在。

例如,interface{ ~[]int }有一个核心类型([]int),但上面的Constraint接口没有核心类型。为了使事情复杂化,当涉及通道操作和某些内置调用(appendcopy)时,上述核心类型定义过于严格。实际规则进行了调整,允许不同的通道方向和包含[]bytestring类型的类型集。

这种方法存在各种问题

  • 由于核心类型的定义必须为不同的语言特性提供健全的类型规则,因此它对于特定操作而言过于严格。例如,Go 1.24 中切片表达式的规则确实依赖于核心类型,因此,即使是有效的,也不允许对由Constraint约束的类型S的操作数进行切片。

  • 在尝试理解特定语言特性时,即使考虑非泛型代码,也可能需要学习核心类型的复杂性。同样,对于切片表达式,语言规范谈论的是被切片操作数的核心类型,而不是简单地声明操作数必须是数组、切片或字符串。后者更直接、更简单、更清晰,并且不需要了解在具体情况下可能不相关的另一个概念。

  • 由于核心类型的概念存在,索引表达式以及lencap(以及其他)的规则,这些都避免了核心类型,在语言中显示为例外而不是规范。反过来,核心类型导致诸如issue #48522之类的提案,该提案将允许选择器x.f访问x类型集中所有元素共享的字段f,这似乎为语言添加了更多例外。没有核心类型,该特性将成为非泛型字段访问普通规则的自然而有用的结果。

Go 1.25

对于即将发布的 Go 1.25 版本(2025 年 8 月),我们决定从语言规范中删除核心类型的概念,转而采用在需要时使用明确的(且等效的!)散文。这有以下几个好处

  • Go 规范呈现的概念更少,使语言更容易学习。
  • 无需参考泛型概念即可理解非泛型代码的行为。
  • 个性化的方法(针对特定操作的特定规则)为更灵活的规则打开了大门。我们已经提到了issue #48522,但也有关于更强大的切片操作以及改进的类型推断的想法。

相关的提案 issue #70128 最近已获批准,相关更改已实施。具体而言,这意味着语言规范中的许多散文已恢复到其最初的泛型前形式,并在需要时添加了新段落以解释与泛型操作数相关的规则。重要的是,没有行为被改变。关于核心类型的整个部分已被删除。编译器的错误消息已更新,不再提及“核心类型”,并且在许多情况下,错误消息现在更具体,通过指出类型集中究竟是哪个类型导致了问题。

以下是一些已进行的更改示例。对于内置函数close,从 Go 1.18 开始,规范如下:

对于核心类型为通道的参数ch,内置函数close记录该通道上将不再发送任何值。

一个只想知道close如何工作的读者,必须首先了解核心类型。从 Go 1.25 开始,这一节将再次以 Go 1.18 之前的方式开始:

对于通道ch,内置函数close(ch)记录该通道上将不再发送任何值。

这更短且更易理解。只有当读者处理泛型操作数时,他们才需要考虑新添加的段落:

如果close参数的类型是类型参数,则其类型集中的所有类型都必须是具有相同元素类型的通道。如果这些通道中有任何是只接收通道,则会报错。

我们在所有提及核心类型的地方都进行了类似的修改。总而言之,尽管这次规范更新不会影响任何当前的 Go 程序,但它为未来的语言改进打开了大门,同时使当前的语言更容易学习,其规范也更简洁。

下一篇文章: 使用 testing.B.Loop 进行更可预测的基准测试
上一篇文章: 抗遍历文件 API
博客索引