# 泛型

Source: https://codewiki.com/zh/go/generics/

> - **what**: 泛型（generic）让函数和类型声明类型参数，同一份实现因而能处理一组具体类型，同时保留输入与输出之间的静态类型关系。
> - **trap**: 约束不是标签，而是编译器允许泛型代码执行的操作集合。`comparable` 只允许 `==` 和 `!=`，不能用来支持 `<`。
> - **fix**: 从实现真正需要的操作反推最小约束；需要接纳具有相同底层类型的命名类型时，在 type term 前写 `~`。

## 是什么，为什么存在

泛型是带有一个或多个类型参数的函数或类型。调用泛型函数或实例化泛型类型时，具体类型会替换这些参数。编译器仍在编译期检查操作、参数和返回值，不需要调用方把结果从 `any` 断言回目标类型。

泛型解决的是跨类型重复但结构相同的代码。切片映射、集合查找和栈等代码只关心元素之间的类型关系，却不应为 `int`、`string` 和每个业务类型各写一份。类型参数把这种关系写进签名，例如 `Map[T, U any]([]T, func(T) U) []U` 表明转换函数的输入来自原切片，输出决定结果切片的元素类型。

接口与泛型解决的问题不同。接口值隐藏一个动态具体类型，适合让调用方通过一组方法使用不同实现；泛型在每次实例化中保留具体静态类型，适合算法和容器。若实现只调用 `Read` 或 `String` 这样的方法，普通接口参数往往更直接；若同一个类型出现在多个参数或返回位置，类型参数通常更能表达契约。

Go 1.18 加入了泛型。你会在标准库的 `slices`、`maps` 和 `cmp` 包、通用数据结构以及避免反射的辅助函数中遇到它。它不是消除所有重复代码的机制：当不同类型需要不同语义时，分别实现通常更清楚。

## 工作原理

### 类型参数保留类型关系

类型参数（type parameter）写在声明名称后的方括号中。`T`、`K` 和 `V` 只是声明内部使用的类型名称；每个名称后都必须有约束。相邻参数约束相同时可以合写成 `[T, U any]`。

泛型函数的普通参数和返回值可以使用这些名称。泛型类型的字段也可以使用它们，而该类型的方法会在接收器中重新声明对应的接收器类型参数，例如 `func (s *Stack[T]) Push(value T)`。这里的 `T` 对应 `Stack` 的类型参数，不是方法新增的独立参数。

实例化包含两个检查。编译器先确认类型实参满足约束，再用这些实参建立具体的函数或类型实例。实例化后的 `Stack[string]` 与 `Stack[int]` 是不同的具名类型，二者不能相互赋值。

### 约束决定可写的操作

泛型约束（generic constraint）是一种接口。它描述允许替换类型参数的 type set，也决定函数体中能对该参数执行哪些操作。约束为 `any` 时，代码只能依赖所有类型都具备的行为，例如赋值、传递和返回。

内置约束 `comparable` 接受可作为 map 键的类型，并允许 `==` 与 `!=`。它没有承诺顺序，所以 `a < b` 无法通过编译。需要顺序运算时，可以使用标准库的 `cmp.Ordered`，也可以声明只覆盖领域所需类型的约束。

约束接口可以嵌入方法、type term 或两者。`fmt.Stringer` 要求 `String() string` 方法；`~int | ~int64` 是一个 union，接纳底层类型为 `int` 或 `int64` 的类型。约束中各项取交集，因此同时写方法和 type term 时，类型必须满足两边。

### 类型集描述候选类型

类型集（type set）是接口所代表的全部非接口类型集合。方法元素保留实现这些方法的类型，union 元素合并候选 type term，嵌入的多个元素再取交集。泛型代码只能使用集合中每个类型都支持且语义一致的操作。

普通 type term `int` 只表示预声明类型 `int`。`~int` 还包括 `type Score int` 这样的命名类型，因为它们的底层类型（underlying type）是 `int`。`~` 不是近似运行时匹配；它是约束语法的一部分，由编译器静态判断。

包含 type term 的接口只能用作约束，不能作为普通变量类型。你可以写 `func Max[T Ordered](...)`，但不能声明 `var value Ordered` 来保存任意有序值。需要装入接口值时，应改用只含方法的基本接口，或明确使用 `any` 并在边界验证动态类型。

### 类型推断减少调用噪声

类型推断（type inference）让编译器从函数实参和约束关系求出省略的类型实参。调用 `Map(ids, strconv.Itoa)` 时，`ids` 给出 `T`，函数值 `strconv.Itoa` 的签名给出 `U`。推断成功后，调用方不必写 `Map[int, string]`。

推断只适用于函数调用等规定的上下文，不会让泛型类型字面量凭字段值推断实参。`Stack[string]{}` 必须写出 `string`。函数调用也可能因没有足够信息而失败，例如某个类型参数只出现在返回值中；这时要显式提供类型实参，或重新设计签名，让输入携带所需关系。

未类型化常量会参与推断和表示性检查。混合不同种类的常量可能得到比预期更宽的默认类型，也可能找不到同时满足约束与实参的类型。API 的示例和测试应包含变量、命名类型和未类型化常量，而不只测试整数字面量。

### 泛型类型仍遵守零值规则

泛型结构体、切片别名式定义和其他具名类型在实例化后仍遵守普通 Go 规则。字段的零值由实际类型实参决定：`var item T` 对 `int` 是 `0`，对指针是 `nil`。当零值有业务含义时，只返回 `T` 无法表示操作是否成功。

泛型容器是否可零值使用，取决于其字段和方法。以切片作为存储的栈可以直接 append，因此零值很好用；以 map 作为存储的集合在第一次写入前必须初始化。构造函数、惰性初始化或文档化的零值契约都可以，关键是只选一种清楚的行为。

## 示例

### 映射时保留输入输出关系

第一个例子把发票编号转成标签。`T` 和 `U` 可以不同，调用点则完全依靠推断。返回类型仍是 `[]string`，无需类型断言。

<!-- quick -->

```go
package main

import (
	"fmt"
	"strconv"
)

func Map[T, U any](values []T, convert func(T) U) []U {
	result := make([]U, len(values))
	for i, value := range values {
		result[i] = convert(value)
	}
	return result
}

func main() {
	invoiceIDs := []int{7, 21, 42}
	labels := Map(invoiceIDs, func(id int) string {
		return "INV-" + strconv.Itoa(id)
	})

	fmt.Println(labels)
}
```

```text
[INV-7 INV-21 INV-42]
```

<!-- /quick -->

`Map` 的实现只分配结果、读取 `T`、调用转换函数并写入 `U`，所以 `any` 已经足够。给它增加数值 union 不会带来可用操作，只会无端拒绝字符串或结构体。这个签名的价值在于保留关系，不在于放宽运行时类型。

### 用 type set 接纳命名类型

这个最大值函数需要 `>`，因此声明真正支持顺序比较的 type set。`Score` 是独立的命名类型，但其底层类型为 `int`；`~int` 让它满足约束。空输入使用额外的布尔值与合法的零值区分。

```go
package main

import "fmt"

type Ordered interface {
	~int | ~int64 | ~float64 | ~string
}

type Score int

func Max[T Ordered](values []T) (T, bool) {
	if len(values) == 0 {
		var zero T
		return zero, false
	}

	best := values[0]
	for _, value := range values[1:] {
		if value > best {
			best = value
		}
	}
	return best, true
}

func main() {
	best, ok := Max([]Score{72, 91, 84})
	fmt.Println(best, ok)

	empty, ok := Max([]Score(nil))
	fmt.Println(empty, ok)
}
```

```text
91 true
0 false
```

如果把 `~int` 改成 `int`，`Score` 就不再满足约束。函数不需要知道实际实参是不是 `Score`；约束已经证明 `>` 对所有候选类型有效。返回的 `best` 仍保持 `Score` 类型。

### 让泛型容器的零值可用

`Stack[T]` 的底层表示是切片。nil 切片可以 append，所以调用方不需要构造函数。`Pop` 返回 `(T, bool)`，避免把空栈与压入的零值混为一谈。

```go
package main

import "fmt"

type Stack[T any] []T

func (s *Stack[T]) Push(value T) {
	*s = append(*s, value)
}

func (s *Stack[T]) Pop() (T, bool) {
	if len(*s) == 0 {
		var zero T
		return zero, false
	}

	last := len(*s) - 1
	value := (*s)[last]
	*s = (*s)[:last]
	return value, true
}

func main() {
	var stages Stack[string]
	stages.Push("review")
	stages.Push("publish")

	for range 3 {
		stage, ok := stages.Pop()
		fmt.Printf("%q %t\n", stage, ok)
	}
}
```

```text
"publish" true
"review" true
"" false
```

接收器写成 `*Stack[T]`，因为 `Push` 和 `Pop` 都要替换切片描述符。元素类型仍由实例化时的 `string` 决定。若把存储改成 map，零值不再能安全写入，API 契约也必须随之改变。

## 陷阱

### 把 `comparable` 当成有序约束

> **陷阱:** `comparable` 只保证 `==` 和 `!=` 可用。生成 `func Min[T comparable](a, b T)` 后在函数体写 `a < b`，代码不会通过编译。

切片、map 和函数不满足 `comparable`；多数标量、指针、通道、接口以及字段均可比较的数组和结构体满足它。但可比较不等于有顺序，布尔值和结构体就是直接反例。

**修复方法：** 使用标准库 `cmp.Ordered`，或声明只包含所需预声明类型及其命名类型的约束。若顺序来自业务规则，应接收比较函数，而不是假定 `<` 能表达该规则。

### 约束太宽却执行额外操作

> **陷阱:** `[T any]` 不会让 `+`、字段选择、索引或方法调用自动可用。函数体只能执行约束对整个 type set 保证的操作。

把代码改成类型 switch 通常只是绕开静态关系，还会漏掉命名类型。它也会让新增类型分支变成运行时维护工作。

**修复方法：** 从函数体需要的操作推导约束。需要方法时嵌入方法接口；需要运算符时声明合适的 type term；其实只需传递值时保留 `any`。

### 忘记 `~` 后拒绝领域类型

> **陷阱:** 约束中的 `int | string` 只包含这两个预声明类型。`type UserID int` 不会因为可转换为 `int` 就自动满足它。

强制调用方先转换成 `int` 会丢失有用的静态类型，还可能让不同领域的标识混在一起。约束是关于类型身份和底层类型的规则，不是一般的转换规则。

**修复方法：** 如果算法对所有底层类型相同的命名类型都有效，使用 `~int`。如果业务上只允许精确的预声明类型，就保留 `int`，并把这种限制写进 API 文档和测试。

### 给方法新增类型参数

> **陷阱:** Go 方法不能声明接收器类型参数之外的新类型参数。`func (s Stack[T]) Map[U any](...)` 是语法错误。

把所有未来可能用到的类型参数提前塞进接收器，会让每个实例化都携带无关参数。它还把一次操作的关系错误地提升为整个类型的身份。

**修复方法：** 需要引入 `U` 时写独立函数，例如 `MapStack[T, U any](Stack[T], func(T) U) Stack[U]`。只有当某个类型参数决定字段或所有方法的长期契约时，才把它放到泛型类型上。

### 用零值表示查找失败

> **陷阱:** 返回单个 `T` 的 `First`、`Min` 或 `Pop` 无法区分失败与有效零值。对字符串是 `""`，对指针是 `nil`，对结构体则是所有字段的零值。

生成代码常用 `var zero T` 避免编译错误，却没有处理调用方如何理解它。这个问题不是泛型独有，但类型参数会让零值形状在写函数时不可知。

**修复方法：** 返回 `(T, bool)` 或 `(T, error)`，由调用方显式处理缺失。只有当零值按领域契约就是唯一正确结果时，才返回单个 `T`。

<!-- deep -->

## type set 的组合规则

### 接口元素相交

约束接口的 type set 从所有非接口类型出发，再由每个元素缩小。方法元素保留方法集中含该方法的类型；单个 type term 保留对应类型；嵌入接口取其 type set。一个类型要满足整个约束，必须同时留在每个元素产生的集合中。

这解释了为何约束同时写 `~int` 和 `String() string` 时，不是二选一。候选类型既要以 `int` 为底层类型，又要声明所需方法。预声明类型 `int` 自身没有该方法，因此不满足这个交集；定义了方法的命名整数类型可以满足。

### union 合并候选项

竖线 `|` 只在一个 union 元素内部表示并集。`~int | ~int64 | ~float64` 允许任意一个 term，但约束中的其他元素仍需满足。多项 union 的非接口 term 必须两两不相交，所以 `int | ~int` 非法，因为前者已经包含在后者中。

union 是封闭清单。选择它意味着新增一种数值类型时，约束不会自动接纳；这有时正是 API 所需的稳定边界。如果算法实际依赖行为而不是预声明表示，比较函数或方法约束可能更容易扩展。

### `~` 检查底层类型

近似元素 `~T` 的 `T` 必须是自身的底层类型，且不能是类型参数。`~MyInt` 在 `type MyInt int` 的情况下无效，因为 `MyInt` 的底层类型不是它自身；应写 `~int`。编译器据此纳入所有底层类型相同的命名类型。

底层类型相同不代表值可以在不同命名类型间直接赋值。约束只决定某个类型能否作为实参以及函数体可用哪些操作。实例化之后，参数和返回值仍保留调用方传入的命名类型。

## 推断与实例化的边界

### 函数实参提供方程

编译器把函数形参中出现的类型参数与实参类型统一。若形参是 `[]T` 而实参是 `[]Invoice`，便可求出 `T` 为 `Invoice`。若同一个参数在多个位置出现，各位置必须给出兼容的答案，否则调用失败，而不是选择一个共同的 `any`。

约束还可能提供第二层关系。形如 `[S ~[]E, E any]` 的声明可以先从实参求出 `S`，再从 `S` 的底层切片类型推断 `E`。标准库的泛型切片函数经常使用这种形状，以便既保留命名切片类型，又知道元素类型。

### 返回上下文不是通用推断来源

不要假设赋值目标总能反向决定函数的类型实参。最可靠的公共 API 让普通实参携带推断所需信息。若构造函数只返回 `T` 而没有接收与 `T` 相关的值，调用方通常必须显式写出类型实参。

部分类型实参列表可以提供开头的参数，让编译器推断剩余参数，但这会让参数顺序成为易用性的一部分。把调用方最可能显式提供的类型参数放在前面，并用真实调用测试推断，比只看声明是否漂亮更可靠。

### 泛型类型必须显式实例化

使用泛型类型时需要实例化，字段值不会替你补上类型实参。写 `Pair[int, string]{...}`、`var stack Stack[Task]` 或让一个已经实例化的值参与普通赋值都可以。只写 `Stack{}` 不是借助上下文的简写，而是遗漏类型实参。

实例化产生的类型继续参与普通的可赋值性和方法集规则。接收器是 `*Stack[T]` 的方法属于相应指针类型的方法集；泛型并没有改变值接收器与指针接收器的区别。审查接口实现时，仍应分别检查 `Container[T]` 和 `*Container[T]`。

## 泛型 API 的取舍

### 优先表达关系

好的类型参数通常在签名中出现多次。它可能连接输入元素与回调参数，连接 map 键与查找键，或者让容器方法返回同一种元素类型。只有一次出现且约束为 `any` 的类型参数往往没有提供关系，普通参数或接口可能更简单。

不要仅为避免一次类型断言就引入泛型。接口适合异构值和运行时分派，类型参数适合同一实例化内部的一致类型。选择取决于调用方需要保留什么信息，不取决于哪种语法更新。

### 让约束保持最小且有意义

约束过宽会让函数体无法表达算法，约束过窄会拒绝合法调用方。最小约束是恰好证明实现所需操作的约束，而不是包含 term 最少的约束。接收比较函数的 `[T any]` 排序器可能比巨大的 `Ordered` union 更通用，因为顺序由调用方提供。

导出的约束也是 API。增加候选类型通常兼容已有调用，但改变可用操作、删除 term 或改变方法要求会破坏使用方。若约束只服务一个函数且没有复用价值，可以保持未导出，减少承诺范围。

### 设计清楚的零值契约

泛型类型无法假定 `T` 的零值可与业务缺失区分。容器查询通常返回 `(V, bool)`，解析或外部操作通常返回 `(T, error)`。这与 map 查询和标准库惯例一致，也让生成代码更容易被测试。

容器自身的零值则是另一个问题。切片存储常能自然支持零值，map、通道或必须配置容量的结构通常需要初始化策略。文档应分别说明元素缺失的返回契约和容器未初始化时的行为。

### 不声称未经测量的性能

泛型的代码生成属于编译器实现细节，不是语言规范给出的固定性能保证。不同类型、方法调用、逃逸行为和编译器版本会得到不同结果，因此不能从语法推导零开销或代码膨胀程度。

若性能决定 API 选择，应在目标 Go 版本、真实类型实参和真实工作负载上写基准，并查看逃逸分析与生成代码。没有这些数据时，只讨论类型安全、可读性和维护成本；这些是可以从源码直接审查的性质。

## 验证泛型边界

泛型实现常在一个常见类型上显得正确，却在约束边缘失败。验证工作应从声明承诺的 type set 出发，而不是从当前调用方碰巧使用的 `int` 出发。编译测试与运行测试各自覆盖不同问题，两者都需要。

### 编译期用例检查约束

为每个应支持的类型写一个最小实例化，并让包在 CI 中编译。用例至少应包含预声明类型、一个底层类型相同的命名类型，以及约束明确排除的类型。最后一类可以放进专门的编译失败测试或分析工具测试，不能留成破坏普通测试套件的源码。

编译成功只证明候选类型满足约束，不能证明算法结果正确。`Max([]Score{...})` 仍需要检查顺序、重复值和空输入。反过来，只有 `int` 运行测试也发现不了约束错误地排除了 `Score`。

### 调用点检查推断

测试应使用文档准备展示的调用形式。若示例省略类型实参，就原样编译该调用，而不是在测试中写出完整的 `[int, string]` 后声称推断有效。函数值、nil 实参和未类型化常量最容易改变推断结果。

重构参数顺序或把值移到返回位置后，应重新编译所有代表性调用。函数体可能完全不变，但推断所需的信息已经消失。API 的易用性存在于调用点，不只存在于声明。

### 运行时用例检查零值

每个返回 `T` 的失败路径都要使用合法零值作为成功数据测试。对 `Stack[string]`，应真的压入 `""`；对 `Stack[*Task]`，应考虑业务上是否允许压入 nil 指针。只有这样才能确认额外的布尔值或错误被正确处理。

容器也要从未经构造的零值开始测试。切片实现可能正常工作，map 实现则可能在写入时 panic。这个测试能阻止内部表示改变后，公开的零值契约悄悄失效。

### 工具检查不能替代契约检查

`go test` 负责运行行为测试并编译实例化路径，`go vet` 检查一部分可疑构造。两者都不会判断约束是否比业务契约更窄，也不会告诉你一个类型参数是否根本没有表达关系。这些问题仍需要按签名逐项审查。

测试矩阵不必枚举 type set 中的每个类型。选择能区分规则的代表：精确 term 与近似 term、零值与非零值、可推断与需显式实参的调用。每个用例都应对应一条可能失败的声明，而不是为了数量重复同一种证据。

### 验证包边界

在使用方包中至少编译一个调用，因为导出名称、类型推断和未导出约束的行为只有跨越包边界后才完整呈现。包内测试可能因可见额外名称而漏掉 API 问题。

保持调用用例很小，并让失败指向一项契约。它们是接口兼容性测试，不是另一套算法测试。

<!-- /deep -->

[检查点: go/generics](https://codewiki.com/zh/go/generics/#checkpoint)

## 延伸阅读

- [Go 语言规范：类型参数声明](https://go.dev/ref/spec#Type_parameter_declarations)
- [Go 教程：泛型入门](https://go.dev/doc/tutorial/generics)
- [Go 博客：泛型简介](https://go.dev/blog/intro-generics)
- [标准库 `cmp.Ordered` 文档](https://pkg.go.dev/cmp#Ordered)
