关于 go-release
go-release 是一项面向 Go 模块与二进制应用发布的工程技能,供维护者规划版本、兼容性和分发流程。它涵盖 API 变更评估、弃用迁移、go.mod 管理及发布检查清单。
主要功能
- 01评估 Go API 变更的兼容性
- 02规划弃用与迁移路径
- 03管理 go.mod 版本和依赖
- 04检查二进制分发流程
- 05提供发布前验证清单
适用场景
- 01为 Go 模块规划版本发布
- 02判断导出 API 变更是否破坏兼容性
- 03准备弃用功能及迁移说明
- 04检查发布标签前的构建与测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spf13/ go-skills --skill go-release安装后在 Claude Code 中输入/go-release确认已加载。
附带文件.claude-plugin/plugin.json
详细介绍
Go 发布工程:版本、兼容性与分发
一次发布就是一项承诺。版本一旦打上标签并通过模块代理获取,就不可更改,而总有人会永远依赖它。本指南介绍如何兑现这项承诺——涵盖库(API 兼容性)和应用(二进制分发)。
何时启用
- 发布或规划 Go 模块或二进制文件的版本
- 审查某项更改是否会破坏已发布的库
- 弃用或移除导出的 API
- 以会影响依赖方的方式编辑
go.mod(go 指令、依赖项) - 设置二进制分发(GoReleaser、ldflags、多平台构建)
- 从糟糕的发布中恢复
兼容性承诺
Go 中的版本语义
- v0.x.y — 不承诺兼容性。可以随意进行破坏性更改,但要在发行说明中记录。对于广泛使用的库来说,无限期停留在 v0 是一种推卸责任的做法:依赖方理应得到承诺。
- v1.x.y — 这是承诺:针对 v1.0.0 编写的代码,可以通过后续所有 v1 版本的编译。次要版本添加功能,补丁版本修复问题。不会移除任何内容,也不会进行不兼容更改。永远不会。
- v2+ — 一个使用不同导入路径的不同模块(
module github.com/you/lib/v2,导入路径为.../lib/v2)。这就是语义化导入版本控制,不能省略——对于模块模式的用户来说,如果 v2 标签没有对应的/v2模块路径,就会出现问题。
计划永远停留在 v1。 主版本升级会拆分你的生态系统:每个依赖方都必须手动迁移,而且传递依赖会在同一次构建中混用 v1 和 v2。Cobra、pflag 和标准库通过扩展而非替换,保持 v1 已有十年。只有在 API 确实无法通过添加内容修复时,才考虑 v2——并将其视为推出一个新库,因为这就是它的代价。
哪些更改实际上会造成破坏
大模型和人类对此都容易判断失误。以下是编译器可检测到的破坏性更改:
| 更改 | 是否造成破坏? | 原因 |
|---|---|---|
| 移除或重命名任何导出的标识符 | 是 | 显而易见 |
| 更改函数/方法签名 | 是 | 包括添加可变参数——签名会发生变化 |
| 向导出的接口添加方法 | 是 | 所有外部实现者都将不再满足该接口 |
| 向结构体添加字段 | 通常不会 | 会破坏使用未指定字段名的复合字面量的调用方以及精确类型赋值;如果已说明只能使用指定字段名的字面量,则安全 |
| 向结构体添加方法 | 不会 | 除非它与其代码中嵌入类型提升的方法冲突——这种情况很少见,属于可接受的风险 |
| 添加新函数、类型或包 | 不会 | 次要版本就是为此而设 |
| 更改无类型常量的值 | 是 | 常量会编译进调用方的二进制文件 |
| 收紧输入验证 | 行为上会造成破坏 | 原本可正常运行的代码现在会报错——应视为破坏性更改 |
| 更改错误消息 | 不会 | 但会破坏依赖字符串匹配的代码;应改为提供可通过 errors.Is 判断的哨兵错误 |
提高 go.mod 中的 go 指令版本 | 实际上会造成破坏 | 使用较旧工具链的依赖方将无法再构建你的模块——参见 go.mod 规范 |
**接口陷阱值得特别强调:**如果你的库导出了由用户实现的接口,那么该接口在 v1 中就被冻结了。设计时要考虑这一点——要么让导出的接口尽可能小且稳定,要么添加未导出的方法,使其只能由你实现;或者接受函数/结构体,而不是直接定义接口(接口应由消费者定义——参见 go 技能)。
通过工具验证,不要只靠目测
# Before every tag — compares against the latest released version
go run golang.org/x/exp/cmd/gorelease@latest
# Or compare two specific API surfaces
go run golang.org/x/exp/cmd/apidiff@latest old/ new/
gorelease 会报告不兼容的更改,并建议正确的下一个版本号。在每个涉及导出标识符的 PR 中,都应在 CI 中运行它——在审查时发现破坏性更改不费成本;等打完标签再发现,就得升级主版本或撤回发布。
弃用:保留兼容性的出口
你不能移除,但可以弃用。这一约定可由机器读取,并受到 gopls、staticcheck 和 pkg.go.dev 遵循:
// Deprecated: Use [ParseContext] instead, which supports cancellation.
// This function will be maintained but not extended.
func Parse(input string) (*Result, error) { ... }
- 段落必须以
Deprecated:开头,才能被识别。 - 弃用的 API 在 v1 上会永远继续工作——弃用会引导新代码使用其他接口,但不代表可以移除旧接口。
- 始终注明替代方案。没有迁移路径的弃用只是一种侮辱。
- 在次要版本中弃用,绝不要在补丁版本中悄悄弃用。
不仅要提供替代方案名称,还要提供迁移方式
注明替代方案只是弃用工作的一半。在 Go 1.26+ 中,go fix 是 Go 的现代化工具所在之处——它包含一套 go vet 风格的分析器,以及由 //go:fix inline 指令驱动的源码级内联器(历史上的修复工具已在 1.26 中移除)。为一个精简的弃用包装器添加注释后,go fix 就会将调用方的代码重写为使用替代方案:
// Deprecated: Use [ParseContext] instead, which supports cancellation.
//
//go:fix inline
func Parse(input string) (*Result, error) {
return ParseContext(context.Background(), input)
}
- 将指令放在一个函数体仅包含对替代方案的一次委托调用的包装器上;内联器会将每个调用点重写为直接使用替代方案。
- 在发布说明中将
go fix ./...列为 Go 1.26+ 下游依赖方的迁移步骤。附带可执行重写的弃用可以通过一次提交清理完毕;只附带文字建议的弃用则会拖上好几年。 - Go 1.27 新增了
atomictypes、embedlit、slicesbackward和unsafefuncs现代化工具,并将waitgroup分析器重命名为waitgroupgo。出于风格方面的考虑,fmtappendf分析器已被移除,因此不再触发——别等它了。
发布时的 go.mod 整洁度
你的 go.mod 是 API 的一部分。通过最小版本选择,你设定的最低版本也会成为依赖方的最低版本。
go指令为每个依赖方设定了最低版本。 将它设为你实际需要的最旧版本,而不是你碰巧正在使用的最新版本。只为使用一个新的标准库辅助函数就提高它,会迫使整个下游依赖树升级——对于库来说,这是一个决策,不是默认选项。在 Go 1.26+ 中,go mod init已会倾向于设定较低版本:1.N.X工具链会写入go 1.(N-1).0,而1.N的预发布版本会写入go 1.(N-2).0。生成的最低版本本身就采取了保守设置——绝不要假定该指令与机器上安装的工具链版本一致;只有经过审慎决定后才提高版本,并在发布说明中注明更改。toolchain指令表示本地偏好,不设定最低版本。 它记录你希望用于构建的工具链,不会限制依赖方。在 CI 中固定工具链没有问题,但要确保存在可用的引导路径:Go 1.26 需要 Go 1.24.6 或更高版本才能引导,而目前的预期是 Go 1.28 将要求 Go 1.26 的某个次要版本或更高版本。- 绝不要为包含
replace指令的版本打标签。 依赖方的构建会忽略这些指令,因此你测试的模块并非他们实际获取的模块。go.work中的假设也一样——工作区不会随模块发布。 - 打标签前运行
go mod tidy。 不整洁的 go.mod 会把虚假的依赖要求带给所有下游用户。在 Go 1.27+ 中,tidy 还会合并go指令为 1.27 或更高版本的模块中的重复require块,在保留附加注释块的同时,确保最多只有两个块(直接依赖和间接依赖)。因此,采用 1.27 后首次运行 tidy 可能会重写 go.mod 中很大一段内容——这种重组是预期行为,不是评审问题。将其单独作为一次更改提交,以便 diff 保持清晰易读。 - 将依赖项的最低版本保持在较低水平。 要求每个依赖项都使用最新补丁版本会迫使依赖方频繁升级;应要求满足需求的最旧版本。
GODEBUG 和 GOEXPERIMENT:有期限,而非永久豁免
当 Go 改变一项无法静默变更的行为时,会先通过 GODEBUG 设置或 GOEXPERIMENT 标志暂时屏蔽这项变更,然后在一两个版本后移除这条退路。移除时间已安排好,不容协商:
asynctimerchan(在 Go 1.23 中添加)将在 Go 1.27 中永久移除——time包的通道现在始终为无缓冲。goroutineleak分析数据已结束实验阶段:在 Go 1.26 中通过GOEXPERIMENT=goroutineleakprofile进行实验,在 Go 1.27 中作为runtime/pprof和/debug/pprof/goroutineleak的正式功能提供,同时删除 GOEXPERIMENT。GOEXPERIMENT=nogreenteagc(Go 1.26,退出 Green Tea GC)和GOEXPERIMENT=nosizespecializedmalloc(Go 1.27,退出按大小特化的分配)都已宣布为临时选项——预计分别在 Go 1.27 和 Go 1.28 中移除。GOEXPERIMENT=nojsonv2(Go 1.27)会恢复 v2 之前的encoding/json实现,也预计会在未来版本中移除。
退出选项是迁移期限,绝不是永久退路:
- 库无法为其依赖方固定行为。 你在 go.mod 中添加的
godebug条目或在源代码中添加的//go:debug注释只对主模块生效;依赖方的构建不会继承这些设置。如果你的发行说明依赖退出选项来为使用者保留旧行为,那么这个承诺已经无法兑现——请利用这段时间迁移自己的代码,并发布采用新行为的版本。 - 使用
godebug条目预览变更。 在 go.mod 的godebug条目中将设置设为新值,并针对将成为默认值的行为进行开发——设置这段过渡期,就是为了让你按自己的进度迁移,而不必跟着版本发布的节奏走。 - Go 1.27 会让过时的固定设置导致构建失败。 现在,
go命令只有在某个GODEBUG设置被设为最终默认值时,才会将其识别为已移除支持的设置;固定为任何旧值的godebug条目或//go:debug注释都会导致构建失败。这就是期限的强制执行机制——固定已移除的退出选项不会保留行为,只会让代码无法编译。 - 在自己的说明中注明移除版本。 当你采用新行为而放弃旧行为时,请说明哪个 Go 版本会移除退出选项,以及依赖方必须做哪些更改。这与弃用 API 时你有义务提供的迁移路径相同。
测试选项也遵循相同规则:testing/cryptotest.SetGlobalRandom 是在测试中获取确定性加密随机数的受支持方式(Go 1.26),而 cryptocustomrand=1 这个 GODEBUG 仅用于暂时恢复旧行为——不要基于它构建测试基础设施。
发布检查清单
go mod tidy && git diff --exit-code go.mod go.sum # no surprises
go build ./...
go vet ./...
go test -race ./...
go run golang.org/x/vuln/cmd/govulncheck@latest ./...
go run golang.org/x/exp/cmd/gorelease@latest # libraries: API check + version suggestion
在 Go 1.27 及更高版本中,如果使用 go 1.27 指令,go mod tidy 可能会合并重复的 require 块(最多保留直接依赖和间接依赖两块,并保留注释)——请预料到这一点,检查其结果,不要误以为这种重组代表无关的依赖变更。
然后:
git tag -a v1.4.0 -m "v1.4.0" # annotated tag, full semver, leading v
git push origin v1.4.0
- 为模块根目录打标签。在多模块仓库中,使用模块目录作为前缀:
submod/v1.4.0。 - 为读者编写发行说明:包含新增内容、弃用内容和行为变更。对任何弃用项,都要链接迁移指南。
- 对于提供了
//go:fix inline包装器的弃用项,迁移说明只需一句话:使用 Go 1.26 或更高版本的依赖方运行go fix ./...。请明确写出这一点——不了解这条命令的人不会因为该指令而自动运行它。 - 在同一版本的发行说明中注明任何平台或最低操作系统版本要求的变更(见下文);即使 API 没有变动,这仍然是支持声明的变更。
代理永久缓存:修复有问题的发布版本
模块代理会永久缓存每个带标签的版本。绝不要删除或重新打标签已发布的版本——代理仍会提供原始版本,此时你的标签与代理中的版本不一致,导致所有人的校验和验证失败。
正确的修复方式是使用 retract:
// go.mod of a NEW release (e.g. v1.4.2)
retract (
v1.4.1 // Broke Parse on empty input; use v1.4.2.
)
在下一次发布中加入撤回声明。go get 会跳过已撤回的版本,而 go list -m -u 会向已在使用这些版本的用户发出警告。甚至可以撤回版本自身(在 v1.5.0 的 go.mod 中加入 retract v1.5.0,然后将其发布为 v1.5.1……这样两个版本都会被撤回)。撤回是一种建议性的弃用——也是唯一存在的此类机制。
应用程序发布:交付二进制文件
对于 CLI 和服务,发布产物是二进制文件,而 GoReleaser 是标准做法——不要在 Shell 脚本或 Makefile 中手动编写交叉编译矩阵。
注入版本信息
将构建时的版本信息传入 CLI 已有的变量中(参见 cobra-viper 技能中的版本处理):
# .goreleaser.yaml
builds:
- env: [CGO_ENABLED=0]
flags: [-trimpath]
ldflags:
- -s -w
- -X github.com/you/app/cmd.version={{.Version}}
- -X github.com/you/app/cmd.commit={{.ShortCommit}}
- -X github.com/you/app/cmd.date={{.Date}}
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
- 使用
CGO_ENABLED=0构建静态、可移植的二进制文件;只有依赖确实需要时才启用 cgo。 -trimpath会从二进制文件中剥离本地路径:文件更小、可复现,也不会在 panic 中泄露/home/you/src/...。-s -w会剥离符号表——这是发布二进制文件的标准做法;如果需要可用的核心转储,则不要使用。
更简单的备用方案:通过 go install 安装时,debug.ReadBuildInfo() 无需任何 ldflags 即可返回模块版本和 VCS 修订版本——在你需要使用 GoReleaser 之前,这是合理的默认选择。
分发检查清单
- 校验和: GoReleaser 默认会生成
checksums.txt——请将其一并发布。 - 签名/来源证明: 等到项目的用户足够多、开始有人询问供应链问题时,再采用 cosign 无密钥签名或 SLSA 来源证明工作流。不要盲目照搬到周末项目中。
go install必须始终可用:go install github.com/you/app@latest是无需基础设施的安装方式。将main放在仓库根目录或cmd/app下,让路径容易猜到,并且绝不要用 replace 指令破坏它。- 包管理器(Homebrew tap、用于 deb/rpm 的 nfpm、Scoop):GoReleaser 可通过配置生成这些内容——用户提出需求时再添加,不要提前添加。
在发布说明中明确平台支持下限
每次 Go 发布都会改变支持的目标平台和最低 OS 版本,而包含已停止支持目标的交叉编译矩阵会交付用户无法运行的产物。在发布矩阵或支持声明前,请先确认以下事项:
- macOS: Go 1.26 是工具链可在 macOS 12 Monterey 上运行的最后一个版本;Go 1.27 要求 macOS 13 Ventura 或更高版本。如果你在 CI 中构建
darwin产物,请说明承诺支持的最低版本。 - Go 1.26 已移除
windows/arm(32 位)——请从.goreleaser.yaml的goos/goarch中移除它。 - Go 1.27 中的
linux/ppc64(大端序)改用了 ELFv2 ABI——在宣称支持该目标前,可能需要更新 cgo 和手写汇编。 freebsd/riscv64已标记为不可用——不要宣传或发布该目标。- 其他值得关注的目标平台变更:Go 1.26 为
linux/riscv64增加了竞态检测器;Go 1.26 中,s390x会通过寄存器传递参数和返回值。
常见错误
- 在次要版本中向已导出的接口添加方法 — 这是典型的无声破坏。运行
gorelease;它会发现这个问题。 - 发布出错后重新打标签 — 代理上已经有旧字节;现在你破坏了全局校验和。撤回发布并推出新版本。
- 打上 v2.0.0 标签,却不将模块路径改为
/v2— 标签虽然存在,但模块模式下的使用者无法使用它。 - 随意提高 go 指令版本 — 这会迫使每个依赖方升级工具链。
- 发布包含
replace的 go.mod — 依赖方使用的依赖版本会与你测试过的不同。 - 在文档和测试中将错误消息文本视为稳定 API — 导出哨兵错误;消息措辞可以由你更改。
- 在 CI 中手动编写
GOOS/GOARCH构建循环 — GoReleaser 已经存在;自定义矩阵只会增加复杂性,没有收益。 - 打标签前跳过
go mod tidy— 这是发布后唯一无法补救的整洁性步骤。