再见 `core types`,你好,我们所熟悉和喜爱的 Go!
The Go Blog
再见 core types
,你好,我们所熟悉和喜爱的 Go!
Robert Griesemer 2025年3月26日
Go 1.18 版本引入了泛型,并随之带来了一些新特性,包括类型参数、类型约束以及类型集合等新概念。它还引入了一个名为 core type
(核心类型)的概念。虽然前述特性提供了具体的新功能,但 core type
是一个抽象的结构,其引入是为了方便和简化处理泛型操作数(类型为类型参数的操作数)。在 Go 编译器中,过去依赖于操作数的底层类型的代码,现在必须调用一个计算操作数 core type
的函数。在语言规范中,我们只需要在很多地方将“底层类型”替换为“core type
”。 看起来不错,不是吗?
事实证明,问题还真不少! 为了理解我们是如何走到这一步的,简要回顾一下类型参数和类型约束的工作方式是很有帮助的。
类型参数和类型约束¶
类型参数是未来类型实参的占位符; 它的作用类似于一个_类型变量_,其值在编译时已知,类似于命名常量代表一个在编译时已知其值的数字、字符串或布尔值。 像普通变量一样,类型参数也有一个类型。 该类型由它们的_类型约束_来描述,类型约束决定了对类型为相应类型参数的操作数允许执行哪些操作。
任何实例化类型参数的具体类型都必须满足类型参数的约束。 这确保了类型为类型参数的操作数具有相应类型约束的所有属性,无论使用什么具体类型来实例化类型参数。
在 Go 中,类型约束通过方法和类型要求的组合来描述,它们共同定义了一个_类型集合_:这是满足所有要求的所有类型的集合。 Go 为此目的使用了广义形式的接口。 接口枚举了一组方法和类型,并且由此类接口描述的类型集合由所有实现这些方法并包含在枚举类型中的类型组成。
例如,接口描述的类型集合
type Constraint interface {
~[]byte | ~string
Hash() uint64
}
由其表示形式为 []byte
或 string
并且其方法集合包括 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
的类型约束(类型集合)包含 []byte
和 string
类型,对于这些类型,使用 i
进行索引是有效的。
Core types
¶
这种基于类型集合的方法非常灵活,并且符合原始泛型提案的意图:如果涉及泛型类型操作数的操作对于相应类型约束允许的任何类型都有效,那么该操作应该是有效的。 为了简化实现的考虑,知道我们以后可以放宽规则,因此_没有_普遍选择这种方法。 相反,例如,对于发送语句,规范指出
通道表达式的
core type
必须是一个通道,通道方向必须允许发送操作,并且要发送的值的类型必须可以分配给通道的元素类型。
这些规则基于 core type
的概念,core type
的定义大致如下:
- 如果一个类型不是类型参数,那么它的
core type
只是它的底层类型。 - 如果类型是类型参数,则
core type
是类型参数类型集合中所有类型的单个底层类型。 如果类型集合具有_不同_的底层类型,则core type
不存在。
例如,interface{ ~[]int }
有一个 core type
([]int
),但上面的 Constraint
接口没有 core type
。 更复杂的是,当涉及到通道操作和某些内置调用(append
、copy
)时,上述 core type
的定义过于严格。 实际规则有调整,允许不同的通道方向和包含 []byte
和 string
类型的类型集合。
这种方法存在各种问题:
- 因为
core type
的定义必须为不同的语言特性带来合理的类型规则,所以它对于特定操作来说过于严格。 例如,Go 1.24 切片表达式的规则确实依赖于core type
,因此,即使它可能是有效的,也不允许对受Constraint
约束的类型S
的操作数进行切片操作。 - 当试图理解一个特定的语言特性时,即使在考虑非泛型代码时,也可能需要学习
core type
的复杂性。 同样,对于切片表达式,语言规范讨论了切片操作数的core type
,而不是仅仅说明操作数必须是数组、切片或字符串。 后者更直接、更简单、更清晰,并且不需要知道在具体情况下可能不相关的另一个概念。 - 因为
core type
的概念存在,所以索引表达式以及len
和cap
(以及其他所有避免core type
的表达式)的规则在语言中表现为异常,而不是规范。 反过来,core type
导致了诸如 issue #48522 之类的提案,该提案允许选择器x.f
访问x
的类型集合的所有元素共享的字段f
,从而向该语言添加更多异常。 如果没有core type
,该功能将成为非泛型字段访问的普通规则的自然且有用的结果。
Go 1.25¶
对于即将发布的 Go 1.25 版本(2025 年 8 月),我们决定从语言规范中删除 core type
的概念,转而采用在需要时明确的(并且等效的!)文字描述。 这有多种好处:
- Go 规范呈现的概念更少,使得学习该语言更容易。
- 无需参考泛型概念即可理解非泛型代码的行为。
- 个性化的方法(针对特定操作的特定规则)为更灵活的规则打开了大门。 我们已经提到了 issue #48522,但也有关于更强大的切片操作和改进的类型推断的想法。
相应的提案 issue #70128 最近已获批准,并且相关的更改已实施。 具体来说,这意味着语言规范中的大量文字恢复到了其原始的、预泛型的形式,并在需要时添加了新的段落以解释适用于泛型操作数的规则。 重要的是,没有改变任何行为。 整个关于 core type
的部分都被删除了。 编译器的错误消息已更新为不再提及 “core type
”,并且在许多情况下,错误消息现在更加具体,可以通过指出类型集合中究竟是哪种类型导致了问题。
这是一个更改示例。 对于内置函数 close
,从 Go 1.18 开始,规范开始如下:
对于
core type
是通道的参数ch
,内置函数close
记录不再在该通道上发送任何值。
只想知道 close
如何工作的读者必须首先了解 core type
。 从 Go 1.25 开始,本节将再次以与 Go 1.18 之前相同的方式开始:
对于通道
ch
,内置函数close(ch)
记录不再在该通道上发送任何值。
这更短且更易于理解。 只有当读者处理泛型操作数时,他们才必须考虑新添加的段落:
如果
close
的参数的类型是类型参数,则其类型集合中的所有类型都必须是具有相同元素类型的通道。 如果这些通道中的任何一个都是只接收通道,则会发生错误。
我们对每个提到 core type
的地方都做了类似的更改。 总之,虽然此规范更新不会影响任何当前的 Go 程序,但它为未来的语言改进打开了大门,同时使当今的语言更容易学习,并使其规范更简单。