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 成为可能(演练场

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

索引操作 s[i] 是允许的,因为 s 的类型是 bytestring,而 bytestring 的类型约束(类型集)包含了 []bytestring 类型,对于这些类型,使用 i 进行索引是有效的。

核心类型

这种基于类型集的方法非常灵活,并且符合原始泛型提案的意图:涉及泛型类型操作数的操作,如果在相应类型约束允许的任何类型下都有效,那么该操作就应该有效。为了简化实现,并知道我们以后可以放宽规则,这种方法并未被普遍采用。相反,例如,对于发送语句,规范规定

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

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

  • 如果一个类型不是类型参数,其核心类型就是其底层类型
  • 如果类型是类型参数,核心类型是类型参数类型集中所有类型的唯一底层类型。如果类型集包含不同的底层类型,则核心类型不存在。

例如,interface{ ~[]int } 有一个核心类型([]int),但上面的 Constraint 接口没有核心类型。更复杂的是,对于通道操作和某些内置函数调用(append, copy),上述核心类型定义过于严格。实际规则进行了调整,允许不同的通道方向以及包含 []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 程序,但它为未来的语言改进打开了大门,同时也使得今天的 Go 语言更容易学习,其规范也更简单。

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