
Go 1.21 发布于 2023-08-08。这一版把很多泛型时代常用工具正式收进标准库，同时引入工具链自动管理，让 Go 版本选择从外部约定变成项目配置的一部分。

源码侧 `api/go1.21.txt` 有 426 条公开 API 增量；公开标准库目录新增 `cmp`、`log/slog`、`maps`、`slices`、`testing/slogtest`。

## 主要变化

### 1. `min`、`max`、`clear` 进入内置函数

`min` 和 `max` 解决了基础比较函数长期缺位的问题，支持整数、浮点数和字符串这类有序类型。过去代码里常见三种写法：手写 `if`、引入工具包、或者为不同类型各写一份 helper。现在简单比较可以直接写：

```go
timeout := min(userTimeout, maxTimeout)
name := min(a.Name, b.Name)
```

`clear` 的行为分两类：对 map 调用会删除所有键值；对 slice 调用会把现有长度范围内的元素置为零值，但不会改变 slice 的长度和容量。

```go
clear(cache)      // map 变空
clear(buf[:used]) // slice 元素归零，len/cap 不变
```

这个细节很重要：`clear(s)` 不是 `s = nil`，也不是 `s = s[:0]`。它适合复用底层数组、释放元素引用、避免对象继续被 GC 视为可达。

### 2. `log/slog` 提供标准结构化日志

`log/slog` 把结构化日志的几个核心概念放进标准库：`Logger` 负责提供调用入口，`Handler` 负责决定记录如何输出，`Record` 表示一条日志，`Attr` 表示结构化字段。

```go
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("request finished",
    "method", r.Method,
    "path", r.URL.Path,
    "status", status,
)
```

它能简化两类操作。第一，库代码可以接收 `*slog.Logger` 或使用默认 logger，而不是绑定 zap、zerolog、logrus 里的某一个。第二，日志字段是结构化的，不需要把 `key=value` 拼成字符串，后续进入日志系统也更容易查询。

`slog` 的扩展点在 `Handler`。如果要接入已有日志平台，可以实现 `Enabled`、`Handle`、`WithAttrs`、`WithGroup`。`testing/slogtest` 则用于验证自定义 handler 是否满足接口语义，避免漏掉分组、属性覆盖、并发安全这些细节。

### 3. `slices`、`maps`、`cmp` 补齐泛型工具

泛型发布后，标准库开始补齐集合操作。`slices` 处理切片，`maps` 处理 map，`cmp` 处理有序比较。

常见简化包括：

```go
xs = slices.Delete(xs, i, j)
ys := slices.Clone(xs)
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.ID, b.ID)
})

dst := maps.Clone(src)
maps.Copy(dst, defaults)
```

这些 API 的价值不只是少写几行。`slices.Delete` 明确表达删除区间，`slices.Clone` 明确表达复制底层数组，`maps.Copy` 明确表达覆盖写入。读代码时能直接看到意图，而不是反复检查 `append`、`copy`、循环赋值有没有边界错误。

`cmp.Compare` 的返回值约定是负数、零、正数，刚好适配 `slices.SortFunc`。这让按单字段排序非常短，也减少比较函数写反的概率。

### 4. 工具链版本写进模块语义

Go 1.21 重新定义了 `go.mod` 里版本相关信息的角色。

`go` 行表示模块使用的语言版本和最低工具链要求。例如：

```go
module example.com/app

go 1.21
```

含义不是“作者电脑上用的是 1.21”，而是“这个模块按 Go 1.21 的语言和标准库语义理解”。Go 1.21 开始，工具链会更认真地对待这条约束：如果当前 `go` 命令太旧，无法理解这个模块要求的版本，就应该停止，而不是用旧语义继续尝试。

`toolchain` 行是建议使用的具体工具链：

```go
toolchain go1.21.3
```

它和 `go` 行的区别是：`go` 行偏最低语义要求，`toolchain` 行偏实际执行建议。比如模块可以声明 `go 1.21`，同时建议 `toolchain go1.21.3`，表示语言语义是 1.21，但希望使用包含补丁修复的 1.21.3 工具链。

行为变化主要由 `GOTOOLCHAIN` 控制：

- `GOTOOLCHAIN=auto`：允许 Go 命令按 `go` 或 `toolchain` 行选择合适工具链，必要时下载。
- `GOTOOLCHAIN=local`：只使用当前本地 Go 命令，不自动切换。
- `GOTOOLCHAIN=path`：只在 `PATH` 中查找目标工具链，不从网络下载。
- `GOTOOLCHAIN=go1.21.3+auto`：以指定版本作为默认值，同时允许按模块要求选择更合适的工具链。

下载的工具链通过 `golang.org/toolchain` 模块分发，因此它进入了 Go 模块下载、代理和校验体系。这个设计把“Go 版本选择”从人工文档变成了 Go 命令可执行的规则。

## 语言与规范

Go 1.21 新增三个内置函数：`min`、`max` 和 `clear`。`clear` 可清空 map 或把 slice 元素置零。语言规范还更精确定义了包初始化顺序，并调整了 `panic(nil)` 的语义。

`panic(nil)` 的变化容易被忽略。新的语义下，直接或间接调用 `panic(nil)` 不再让 `recover` 返回 nil，而是返回一个表示 nil panic 的非 nil 值。这样可以区分“没有 panic”和“确实发生了 panic，只是参数是 nil”。这让 panic/recover 包装代码更可靠。

行为变化要看这类封装：

```go
defer func() {
    if v := recover(); v != nil {
        err = fmt.Errorf("panic: %v", v)
    }
}()

panic(nil)
```

旧语义下，`recover()` 返回 nil，封装层会把它当成“没有 panic”。新语义下，`recover()` 返回非 nil 值，封装层能识别出 panic 真的发生过。依赖 `recover() == nil` 判断控制流的代码，需要理解这个差异。

包初始化顺序也被规范说得更精确。对普通程序影响不大，但如果多个文件的 `init`、包级变量初始化和隐式依赖关系交织在一起，初始化顺序不应该依赖文件系统返回顺序或工具偶然排序。更明确的规范让编译器、分析器和生成代码工具有统一依据。

## 工具链与运行时

工具链管理是这一版的工程重点。`go.mod` 中的 `go` 行语义更严格，`toolchain` 行可以指定建议工具链；本地工具链不足时，`go` 命令可以自动选择合适版本。

PGO 在这一版成为可用优化路径。对热点稳定的服务，基于真实 profile 的构建可能带来可观收益。

PGO 的使用路径是先用真实负载采集 CPU profile，再把 profile 交给编译器。编译器可以据此调整内联、函数布局和分支权重。它不改变源码 API，但会改变生成代码的优化决策。对热点集中、调用路径稳定的服务，PGO 比单纯靠微基准更接近真实负载。

## 标准库与新增包

新增公开包：

- `log/slog`：结构化日志。
- `testing/slogtest`：验证 slog handler。
- `slices`、`maps`、`cmp`：泛型集合和比较工具。

其他变化：

- `context` 增加带 cause 的取消能力相关 API，例如 `WithCancelCause` 和 `Cause`。它能保留取消原因，不再只能通过 `ctx.Err()` 得到 `context.Canceled` 或 `context.DeadlineExceeded`。
- `runtime/trace`、`runtime/metrics` 增强可观测性。
- `crypto/tls`、`crypto/x509` 更新安全默认行为。

`WithCancelCause` 的典型用法是把“为什么取消”传给下游：

```go
ctx, cancel := context.WithCancelCause(parent)
cancel(fmt.Errorf("quota exceeded"))

if err := context.Cause(ctx); err != nil {
    slog.Warn("request canceled", "cause", err)
}
```

`ctx.Err()` 仍然只表达取消类别，例如 `context.Canceled`；`context.Cause(ctx)` 才返回业务原因。这能减少用额外 channel 或共享变量传递取消原因的样板代码。

## 小版本特殊变化

Go 1.21 系列共有 13 个小版本。由于这一版引入 toolchain 管理、`slog`、`slices`、`maps`、PGO，小版本里 `cmd/go`、compiler、runtime 和标准库修复都不少。

Go 1.21 之后 `cmd/go` 的安全修复要格外看重，因为工具链自动选择、模块下载和校验行为都更重要了。`cmd/go` 出问题不一定影响运行中的服务，但可能影响“取到什么源码、执行什么 VCS 命令、使用什么工具链”。`crypto/tls`、`crypto/x509` 仍然是 TLS 信任边界；`html/template`、`encoding/gob`、`encoding/xml`、`net/http` 属于不可信输入解析；`net/netip` 的安全修复则和地址解析、前缀判断、网络策略匹配这类逻辑有关。

- `go1.21.1` 是大范围修复：安全修复覆盖 `cmd/go`、`crypto/tls`、`html/template`，Bug 修复覆盖 compiler、`go` 命令、linker、runtime、`context`、`encoding/gob`、`encoding/xml`、`go/types`、`net/http`、`os`、`path/filepath`。
- `go1.21.2` 修复 `cmd/go` 安全问题，同时修 runtime metrics。
- `go1.21.3` 是 `net/http` 安全修复小版本。
- `go1.21.8` 修复 `crypto/x509`、`html/template`、`net/http`、`net/http/cookiejar`、`net/mail` 安全问题。
- `go1.21.10` 修复 `go` 命令安全问题。
- `go1.21.11` 修复 `archive/zip` 和 `net/netip` 安全问题。`net/netip` 是 Go 1.18 新包，这里说明新 API 也会继续经历安全边界打磨。
- `go1.21.13` 修复 `go` 命令、covdata 命令和 `bytes`，是该系列收尾小版本。

## 参考

- [Go 1.21 Release Notes](https://go.dev/doc/go1.21)
- [Go Release History](https://go.dev/doc/devel/release)
- [Go 1.21 source tag](https://go.googlesource.com/go/+/refs/tags/go1.21.0)
- [api/go1.21.txt](https://go.googlesource.com/go/+/refs/tags/go1.21.0/api/go1.21.txt)

