Go 实验详解
摘要
本文介绍了 Go 语言中实验性功能的处理方式、生命周期以及近期实验示例。
<p><a href="https://lobste.rs/s/lmfm5b/go_experiments_explained">评论</a></p>
查看缓存全文
缓存时间: 2026/06/01 14:32
# Go 实验详解 - Alex Edwards
来源: https://www.alexedwards.net/blog/go-experiments-explained
Go 常常会随版本发布一些*实验性功能*。
这些实验性功能有不同形式:有时是标准库中全新的包,有时是对编译器或运行时的改动,或者——极少数情况下——是对 Go 行为的中断性变更。
大多数情况下,实验性功能的目的是在功能正式成为*通用可用*并永久成为 Go 的一部分之前,从用户那里获得真实的反馈。如果该功能导致回归,或收到社区的负面反馈,那么在最终定稿之前可以进行修改——甚至完全放弃。
## 一些例子
我们来看几个最近的例子,说明 Go 实验可能涵盖的类型。
- Go 1.24 发布了新的 `testing/synctest` 包的实验性支持(该包提供对并发代码测试的支持)。经过反馈,该包的 API 稍微调整,并在 Go 1.25 中升级为通用可用。
- Go 1.25 发布了新的 [垃圾回收器](https://github.com/golang/go/issues/73581) 设计的实验性支持,性能更好。在吸收反馈后,新垃圾回收器成为 Go 1.26 的默认选项。
- Go 1.21 发布了循环变量语义的[实验性行为变更](https://go.dev/wiki/LoopvarExperiment)。这个变更修复了 Go 代码中一个常见的 bug,但技术上是对语言的中断性变更。将此变更作为实验发布,让人们有机会在新行为成为 Go 1.22 默认选项之前测试自己的代码。
## 实验生命周期
实验没有单一固定的生命周期,但有一些常见模式。
大多数实验最初以*默认关闭*的形式发布。你需要显式选择加入来试用该功能,通常通过设置 `GOEXPERIMENT` 环境变量(稍后会详细讨论)。
如果一切顺利,在一两个版本之后,实验功能会被最终确定,升级为通用可用,并变为*默认开启*。
如果某个实验影响了某些行为,那么在它升级为通用可用后,有时(但并非总是)会有一个过渡宽限期,在此期间可以暂时禁用它并使用旧行为。例如,在 Go 1.26 中,新的垃圾回收器设计(我们上面简要提到过)升级为通用可用并默认开启,但如果你需要,仍然可以禁用它并使用旧垃圾回收器。
这是最常见的模式,但有时事情会花费更长时间或以不同方式发展。例如:
- Go 1.22 发布了编译器内联逻辑的实验性实现,该实现至今仍默认关闭且正在评估中,已超过两年。
- 同一版本还发布了*内存竞技场*实验。在收到用户的负面反馈和担忧后,它仍然默认关闭,处于[无限期搁置](https://avittig.medium.com/golangs-big-miss-on-memory-arenas-f1375524cc90)状态,最终甚至可能完全移除。
或者,当 Go 团队对某个变更充满信心时,他们可能会跳过反馈阶段直接进入通用可用……但可能仍然有一个过渡宽限期,在此期间可以禁用它。
一个很好的例子是,当 Go 1.24 将其映射实现更改为使用 [Swiss 表](https://go.dev/blog/swisstable) 时。Go 团队对该实现及其性能优势充满信心,因此直接升级为通用可用并默认开启,但——至少目前——如果你愿意,仍然可以选择退出并使用旧的映射实现。
所以实际上,实验大致有三种状态:
- 默认关闭且正在评估
- 默认关闭且搁置/休眠
- 默认开启且可临时退出
## 永久性实验
Go 中还有少数实验性功能实际上并不是通常意义上的“实验”。
这些功能默认关闭,但它们并不是在评估中,也不寻求反馈,也没有期望它们会升级为通用可用并默认开启。
虽然它们与其它实验一样通过 `GOEXPERIMENT` 环境设置控制,但实际上它们更像是可选的 Go 功能,你可能在特殊情况下会用到。
在本文后面,我将把这类实验称为“永久性实验”。
例如,有一个[字段跟踪](https://codereview.appspot.com/6749064)诊断功能,用于跟踪哪些结构体字段被访问。它已经可用十年了,而且[没有意图](https://github.com/golang/go/issues/42712#issuecomment-737414957)让它升级为通用可用。或者有一个[静态锁排序](https://go.googlesource.com/go/+/0a820007e70fdd038950f28254c6269cd9588c02)功能,这是一个用于在 Go 运行时中查找潜在死锁的诊断工具。
## 当前有哪些实验可用?
要找出当前有哪些实验性功能及其状态,出奇地困难。
不幸的是,官方 Go 文档或 [Go Wiki](https://go.dev/wiki/All) 中并没有一个页面来追踪实验状态,为了写这篇文章,我不得不从各个地方拼凑信息。如果你想做同样的事情:
- 你可以运行 `$ go doc goexperiment.Flags` 来获取所有可用实验的列表。
- 你可以阅读 `src/internal/buildcfg/exp.go` 的源代码——特别是查看 `ParseGOEXPERIMENT()` 函数中的 `baseline` 变量声明——来了解哪些实验默认开启。
- 你可以将实验名称与 Go 发行说明进行交叉引用,并搜索 GitHub 上的 issue,以尝试了解当前状态。
据我所知,截至 Go 1.26,以下是可用的永久性实验:
实验名称 | 描述 | 状态
--- | --- | ---
`FieldTrack` | 诊断功能,用于跟踪哪些结构体字段被访问 | 默认关闭,且是[永久性固定项](https://github.com/golang/go/issues/42712#issuecomment-737414957)
`StaticLockRanking` | 诊断功能,用于验证锁获取顺序以捕获死锁 | 默认关闭,永久性固定项
`CgoCheck2` | 诊断功能,用于[检查 cgo 指针传递规则](https://tip.golang.org/doc/go1.21#runtimepkgruntime);默认运行成本太高 | 默认关闭,永久性固定项
`BoringCrypto` | 将 Go 的加密替换为 FIPS 验证的 BoringSSL;自 Go 1.24 起不再相关 | 默认关闭,[永久性固定项](https://github.com/golang/go/issues/42712#issuecomment-737414957),但[很快会被移除](https://go.dev/blog/fips140)
`PreemptibleLoops` | 允许调度器在[循环回边处抢占 goroutine](https://github.com/golang/go/issues/10958);自 Go 1.14 起通常不相关,但在[否则不支持抢占的平台上](https://go.dev/doc/go1.14#runtime)可能仍然有用 | 默认关闭,永久性固定项
以下是当前默认关闭的实验及其状态:
实验名称 | 描述 | 状态
--- | --- | ---
`HeapMinimum512KiB` | 将最小堆大小从 4MB 降低到 512KiB;可能对受限环境有用 | 默认关闭,且[可能处于休眠状态](https://github.com/golang/go/commit/c5c1955077cb94736b0f311b3a02419d166f45ac)
`Arenas` | [内存竞技场](https://uptrace.dev/blog/golang-memory-arena)实现 | 默认关闭,在收到负面反馈后处于[搁置](https://github.com/golang/go/issues/51317)状态
`NewInliner` | 重写的编译器内联器,具有更好的调用点启发式 | 默认关闭,正在评估(自 [Go 1.22](https://go.dev/doc/go1.22#compiler) 起可用)
`JSONv2` | 新的 `encoding/json/v2` 包,提供改进的 JSON 编码/解码功能 | 默认关闭,正在评估(自 [Go 1.25](https://go.dev/doc/go1.25#json_v2) 起可用)
`RuntimeSecret` | 新的 `runtime/secret` 包,提供将内存清零的功能;仅在 Linux amd64/arm64 上可用 | 默认关闭,正在评估(自 [Go 1.26](https://go.dev/doc/go1.26#new-experimental-runtimesecret-package) 起可用)
`GoroutineLeakProfile` | 添加 `goroutineleak` pprof 分析类型 | 默认关闭,正在评估(自 [Go 1.26](https://go.dev/doc/go1.26#goroutineleak-profiles) 起可用)
`SIMD` | 新的 `simd/archsimd` 包,提供对架构特定 SIMD 操作的访问;仅在 amd64 上可用 | 默认关闭,正在评估(自 [Go 1.26](https://go.dev/doc/go1.26#simd) 起可用)
`RuntimeFreegc` | 允许在安全的情况下立即重用内存,无需等待 GC 周期 | 默认关闭,正在评估(自 Go 1.26 起可用,但状态信息请参阅 [#74299](https://github.com/golang/go/issues/74299))
`SizeSpecializedMalloc` | 启用按大小类专门化的 malloc 实现 | 默认关闭,正在评估(自 Go 1.26 起可用,但状态信息请参阅 [#74299](https://github.com/golang/go/issues/74299))
以下是当前默认开启的实验:
实验名称 | 描述 | 状态
--- | --- | ---
`LoopVar` | [每次迭代的循环变量作用域](https://go.dev/wiki/LoopvarExperiment) | 自 [Go 1.22](https://go.dev/doc/go1.22) 起默认开启,但为边缘情况保留了退出选项
`Dwarf5` | DWARF 5 调试信息生成;减小二进制文件大小 | 默认开启,可临时退出(退出选项[可能在将来的版本中移除](https://go.dev/doc/go1.25#dwarf5-support))
`RandomizedHeapBase64` | 在启动时随机化堆基地址,作为安全措施 | 默认开启,可临时退出(退出选项[预计在将来的版本中移除](https://go.dev/doc/go1.26#heap-base-address-randomization))
`GreenTeaGC` | 新的垃圾回收器,性能改进;在 darwin/ios/aix 上不可用 | 默认开启,可临时退出(退出选项[预计在 Go 1.27 中移除](https://go.dev/doc/go1.26#new-garbage-collector))
`RegabiWrappers` | ABI 包装器,用于在 ABI0 和 ABIInternal 函数之间调用;仅在 64 位架构上可用 | 默认开启,可临时退出,但退出选项仅对 s390x 有效,并[将在 Go 1.27 中移除](https://github.com/golang/go/commit/6da07b9b44d2ae08921cb97900f076c96a7bf6fc)
`RegabiArgs` | 在所有编译的 Go 函数中启用寄存器参数/结果;仅在 64 位架构上可用 | 默认开启,可临时退出,但退出选项仅对 s390x 有效,并[将在 Go 1.27 中移除](https://github.com/golang/go/commit/6da07b9b44d2ae08921cb97900f076c96a7bf6fc)
## 如何启用和禁用实验?
实验通过 `GOEXPERIMENT` 环境设置进行控制。
如果你想尝试一些默认关闭的实验,你应该将实验名称作为逗号分隔的*小写*值包含在 `GOEXPERIMENT` 中。例如,如果你想在启用 `JSONv2` 和 `GoroutineLeakProfile` 实验的情况下构建你的应用程序,可以这样做:
`$ GOEXPERIMENT=jsonv2,goroutineleakprofile go build ./...`
如果有一个默认开启的实验你想关闭,可以通过在实验的小写名称前加上 `no` 来实现。例如,如果你想在禁用 `GreenTeaGC` 和 `RandomizedHeapBase64` 实验的情况下构建你的应用程序,可以这样做:
`$ GOEXPERIMENT=nogreenteagc,norandomizedheapbase64 go build ./...`
混合启用和禁用的实验是完全可行的:
`$ GOEXPERIMENT=jsonv2,nogreenteagc go build ./...`
请注意,如果你使用不同的 `GOEXPERIMENT` 值构建同一个包,Go 会将其视为不同的构建,并在构建缓存中存储独立的条目。
我在上面的例子中使用了 `go build`,但你在使用 `go run` 或 `go test` 时也可以使用完全相同的模式。如果你想亲自尝试,可以创建以下程序,它使用了实验性的 `encoding/json/v2` 包:
```go
package main
import (
"encoding/json/v2"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
City string `json:"city"`
}
func main() {
p := Person{Name: "Ada", Age: 36, City: "Vienna"}
data, _ := json.Marshal(p, json.StringifyNumbers(true))
fmt.Println(string(data))
}
```
如果正常运行,程序不会编译,你会得到类似这样的错误信息:
```
$ go run main.go
package command-line-arguments imports encoding/json/v2: build constraints exclude all Go files in /usr/local/go/src/encoding/json/v2
```
但是如果你启用了 `JSONv2` 实验,程序会按预期运行:
```
$ GOEXPERIMENT=jsonv2 go run main.go
{"name":"Ada","age":"36","city":"Vienna"}
```
## 哪些实验是你真正应该关心的?
如果你像我一样是一个普通的 Gopher,主要使用 Go 编写程序而不是在 Go 本身工作,那么大多数可用实验可能与你不太相关。
最有趣且最相关的可能是:
- `GreenTeaGC` – 如果你使用 Go 1.26,你已经默认使用了它。但如果你注意到任何性能或行为问题,值得知道你可以禁用它(并且你也应该提交 issue)。
- `Dwarf5` – 同样,如果你使用 Go 1.25 或更高版本,你已经默认使用了它。但如果你遇到任何问题,知道你可以禁用它是有用的。
- `JSONv2` – 我不建议在它升级为通用可用之前切换到它,但如果你写了很多处理 JSON 的代码,值得尝试新的 `encoding/json/v2` 包,熟悉即将到来的内容,并在发现任何问题时提供反馈。
- `GoroutineLeakProfile` – 这个功能立即有用,如果你怀疑有 goroutine 泄漏并需要调试,值得启用。
- `RuntimeSecret` – 如果你编写加密代码或需要处理敏感数据,值得尝试并反馈意见。
- `RuntimeFreegc` – 如果你的应用程序严重依赖垃圾回收器,可能值得用启用此功能来基准测试代码,看看是否提高了性能,并在发现任何问题时提供反馈。
最后,值得强调的是,实验性功能不受 Go 兼容性承诺的保护。它们的 API、行为和性能特征都可能发生变化,因此通常最好避免过早采用并依赖未最终确定的实验性功能。
但实验性功能通常预示着 Go 中一些最大的变化。如果你知道某个实验在最终成为通用可用并默认开启后很可能影响你或你的代码,那么亲自尝试、在适当的情况下运行基准测试,并在发现问题时提供反馈,是一个好主意。
如果你想跟踪可用实验及其状态,Go 发行说明最近在记录实验性功能及其使用方法方面做得更好了。结合这篇博客文章和在出现新 Go 版本时浏览发行说明,你应该会对情况有不错的了解。
相似文章
Go 1.27 交互式导览
Go 1.27 新功能的实践性交互式导览,重点介绍泛型方法、结构体字面量字段选择器等,并提供基于官方发布说明的可运行示例。
调试挂起的Go程序的技巧
一份实用指南,涵盖了调试挂起的Go程序的三种方法:使用SIGQUIT打印堆栈跟踪、附加delve调试器以及保存核心转储供后续分析。
优化CPU密集型Go热路径的笔记
本文讨论了CPU密集型Go代码的性能优化技术,指出了泛型和接口抽象因无法内联而产生的局限性,并主张在热路径中使用代码复制。文章通过一个Brotli移植示例和深入基准测试进行了说明。
深入理解Go运行时:性能分析
深入探讨Go的性能分析机制,解释运行时如何收集CPU、堆、阻塞、互斥锁和协程的性能分析数据,以及这些数据在pprof格式中的表示方式。
就用Go
一篇带有强烈观点的开发者文章倡导使用Go编程语言,强调其简洁的语法、强大的标准库、高效的并发模型以及单二进制部署,作为对过于复杂的现代技术栈的实用替代方案。