2026 年 2 月发布的 Go 1.26 包含了完全重写的 go fix 子命令。它使用一组算法寻找改进代码的机会,通常会利用语言和标准库中较新的特性。本文先介绍怎样用 go fix 更新 Go 代码库,再说明它背后的基础设施及其演进,最后介绍“自助式”分析工具:帮助模块维护者与组织把自己的指导原则和最佳实践写成可执行的规则。
运行 go fix
与 go build、go vet 一样,go fix 接受表示包集合的模式。下面的命令会修正当前目录之下的所有包:
$ go fix ./...
成功执行后,命令会直接更新源文件,不输出额外消息。凡是涉及生成文件的修正都会被丢弃,因为应当修改的是生成器的逻辑。每次把项目的构建工具链升级到较新的 Go 版本后,都建议运行一次 go fix。它可能改动数百个文件,因此应从干净的 Git 工作区开始,让这次变更只包含工具产生的修改,便于代码审查。
使用 -diff 标志,可以预览上述命令将产生的改动:
$ go fix -diff ./...
--- dir/file.go (old)
+++ dir/file.go (new)
- eq := strings.IndexByte(pair, '=')
- result[pair[:eq]] = pair[1+eq:]
+ before, after, _ := strings.Cut(pair, "=")
+ result[before] = after
…
运行下面的命令,可以列出可用的修正器:
$ go tool fix help
…
Registered analyzers:
any replace interface{} with any
buildtag check //go:build and // +build directives
fmtappendf replace []byte(fmt.Sprintf) with fmt.Appendf
forvar remove redundant re-declaration of loop variables
hostport check format of addresses passed to net.Dial
inline apply fixes based on 'go:fix inline' comment directives
mapsloop replace explicit loops over maps with calls to maps package
minmax replace if/else statements with calls to min or max
…
再加上某个分析器的名字,就会显示它的完整文档:
$ go tool fix help forvar
forvar: remove redundant re-declaration of loop variables
The forvar analyzer removes unnecessary shadowing of loop variables.
Before Go 1.22, it was common to write `for _, x := range s { x := x ... }`
to create a fresh variable for each iteration. Go 1.22 changed the semantics
of `for` loops, making this pattern redundant. This analyzer removes the
unnecessary `x := x` statement.
This fix only applies to `range` loops.
默认情况下,go fix 会运行所有分析器。处理大型项目时,把改动最多的分析器产生的修正分别提交,可以减轻审查负担。若只启用特定分析器,使用与分析器同名的标志。例如,只运行 any 修正器时指定 -any。若要运行所有分析器但排除其中几个,则把相应标志设为假,例如 -any=false。
与 go build 和 go vet 一样,每次 go fix 只分析某一种构建配置。如果项目有很多使用构建标签区分 CPU 或平台的文件,可以使用不同的 GOARCH 和 GOOS 值多运行几次,以提高覆盖程度:
$ GOOS=linux GOARCH=amd64 go fix ./...
$ GOOS=darwin GOARCH=arm64 go fix ./...
$ GOOS=windows GOARCH=amd64 go fix ./...
重复运行命令,还可能触发修正之间的协同效果,后文会介绍。
代码现代化分析器
Go 1.18 引入泛型,结束了语言规范长期少有变化的阶段,开始了变化更快、但仍然谨慎的时期,标准库尤其如此。Go 程序员经常编写的小循环,例如把 map 的键收集到切片中,现在可以用 maps.Keys 之类的泛型函数方便地表达。这些新特性带来了许多简化既有代码的机会。
2024 年 12 月,在大语言模型编程助手被迅速采用期间,我们注意到:这些工具通常会生成与训练语料中大量 Go 代码相似的风格,即使同样的意图已经有更新、更好的写法。这一点并不令人意外。更不明显的问题是,即使给出“始终采用 Go 1.25 最新惯用法”这样的总体要求,它们也常常拒绝使用新写法。有时明确要求使用某项特性,模型仍会否认该特性存在。我的 2025 年 GopherCon 演讲中还有更多这样的例子。要让未来的模型学习最新惯用法,就必须让这些写法出现在训练数据里,也就是全球开源 Go 代码中。
过去一年,我们构建了数十个分析器,用来发现可以现代化的代码。下面是它们提出的三个修正示例。
minmax 会把 if 语句替换成 Go 1.21 引入的 min 或 max:
x := f()
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
x := min(max(f(), 0), 100)
rangeint 会把三段式 for 循环替换成 Go 1.22 支持的整数 range 循环:
for i := 0; i < n; i++ {
f()
}
for range n {
f()
}
stringscut 会把 strings.Index 加切片操作替换成 Go 1.18 的 strings.Cut。前面的 -diff 输出已经展示过它:
i := strings.Index(s, ":")
if i >= 0 {
return s[:i]
}
before, _, ok := strings.Cut(s, ":")
if ok {
return before
}
这些分析器既包含在 gopls 中,在输入代码时即时提供反馈,也包含在 go fix 中,让你用一条命令更新多个完整的包。除了让代码更清楚,它们还可以帮助 Go 程序员了解较新的特性。现在,语言和标准库的每一项新改动进入批准流程时,提案审查组都会考虑是否应当同时提供对应的现代化分析器。我们预计今后的每个版本都会加入更多分析器。
示例:Go 1.26 的 new(expr) 分析器
Go 1.26 对语言规范做了一个小而实用的改动。内置函数 new 会创建一个新变量并返回它的地址。过去,它唯一的实参必须是类型,例如 new(string),新变量则初始化为该类型的零值,例如 ""。在 Go 1.26 中,new 可以接收一个值,用这个值初始化新变量,从而省去额外的赋值语句。例如:
ptr := new(string)
*ptr = "go1.25"
ptr := new("go1.26")
这一特性填补了一个讨论超过十年的空缺,也解决了一项很受欢迎的语言修改提案。对于用指针类型 *T 表示可选的 T 值的代码,它尤其方便;这在 json.Marshal 或 Protocol Buffers 等序列化场景中很常见。由于这个模式经常出现,人们通常会编写辅助函数,比如下面的 newInt,让调用者无需离开表达式上下文再添加语句:
type RequestJSON struct {
URL string
Attempts *int // (optional)
}
data, err := json.Marshal(&RequestJSON{
URL: url,
Attempts: newInt(10),
})
func newInt(x int) *int { return &x }
处理 Protocol Buffers 时经常需要 newInt 一类的辅助函数,因此 proto API 本身就提供了 proto.Int64、proto.String 等函数。但 Go 1.26 让这些辅助函数不再必要:
data, err := json.Marshal(&RequestJSON{
URL: url,
Attempts: new(10),
})
为了帮助你采用这个特性,go fix 现在包含 newexpr 修正器。它识别 newInt 之类行为类似 new 的函数,并建议把函数体改成 return new(x);无论调用来自同一个包还是导入该包的其他包,它都会建议把调用替换成直接的 new(expr)。
为了避免过早引入新特性,分析器只会对要求至少使用相应最低 Go 版本的文件提出修正。这里的最低版本是 Go 1.26;该要求可以通过所在模块 go.mod 文件中的 go 1.26 指令,或文件自身的 //go:build go1.26 构建约束来表达。
运行下面的命令,可以更新源代码树中所有这种形式的调用:
$ go fix -newexpr ./...
顺利的话,newInt 一类的辅助函数此时已没有调用者,可以安全删除,前提是它们不属于已经发布且需要保持稳定的 API。少量不宜自动修正的调用可能仍会保留,例如局部声明遮蔽了 new 这个名字。也可以使用 deadcode 命令帮助寻找未使用的函数。
协同修正
一次现代化改动可能为另一项改动创造机会。例如,下面的代码把 x 限制在 0 到 100 的范围内。minmax 首先建议使用 max;应用该修正后,它又会提出使用 min 的第二次修正。
x := f()
if x < 0 {
x = 0
}
if x > 100 {
x = 100
}
x := min(max(f(), 0), 100)
不同分析器之间也可能产生协同效果。例如,在循环中反复拼接字符串是一个常见错误,会导致二次方时间复杂度,既可能成为性能缺陷,也可能成为拒绝服务攻击的入口。stringsbuilder 能识别这个问题,并建议使用 Go 1.10 引入的 strings.Builder:
s := ""
for _, b := range bytes {
s += fmt.Sprintf("%02x", b)
}
use(s)
var s strings.Builder
for _, b := range bytes {
s.WriteString(fmt.Sprintf("%02x", b))
}
use(s.String())
应用这一修正后,第二个分析器可能发现 WriteString 与 Sprintf 可以合并为 fmt.Fprintf(&s, "%02x", b),写法更清楚、效率也更高,于是提出第二次修正。这里的第二个分析器是 Dominik Honnef 的 Staticcheck 中的 QF1012。原文发表时,它已经在 gopls 中启用,但尚未包含在 go fix 中;作者当时计划从 Go 1.27 起把 Staticcheck 分析器加入 go 命令。
因此,可以考虑多运行几次 go fix,直到不再产生新改动。通常两次就足够。
合并修正与冲突
一次 go fix 可能在同一个源文件里应用数十项修正。从概念上看,这些修正彼此独立,类似一组拥有同一个父提交的 Git 提交。工具使用简单的三方合并算法依次协调这些修正,类似合并多个修改同一文件的提交。如果某项修正与此前积累的改动冲突,工具会丢弃它,并警告有修正被跳过、需要再次运行。
这个方法能够可靠地发现改动区域重叠造成的语法冲突,但还存在另一类冲突:两个改动在文本上互不重叠,却在含义上互不兼容,这就是语义冲突。例如,两项修正各自删除一个局部变量倒数第二处使用中的一处。单独应用任何一项都没有问题,但一起应用后,变量就完全没有被使用,在 Go 中会造成编译错误。两项修正都不负责删除变量声明,但总要有人处理,而这个人就是 go fix 的使用者。
多项修正导致某个导入不再使用时,也会产生类似的语义冲突。由于这种情况很常见,go fix 会在最后再执行一轮检查,自动删除未使用的导入。
语义冲突相对少见。好在它们通常会表现为编译错误,很难被遗漏;但一旦出现,运行 go fix 后仍然需要做一些人工处理。
接下来看看这些工具背后的基础设施。
Go 分析框架
从 Go 的早期开始,go 命令就有用于静态分析的 go vet 和 go fix 两个子命令,各自运行一组“检查器”和“修正器”。检查器会报告代码中可能出现的错误,例如把字符串而非整数传给 fmt.Printf("%d") 的格式化操作。修正器则安全地编辑代码,修复缺陷,或用更清楚、简洁、高效的方式表达相同意图。如果同一种算法既能报告问题,又能安全修复,它有时会同时出现在两组工具中。
2017 年,我们重新设计了当时还是单体程序的 go vet,把检查算法(现在称为“分析器”)与负责运行它们的“驱动程序”分开,形成了 Go 分析框架。这样,分析器只需编写一次,就能通过不同驱动程序运行在各种环境中,例如:
unitchecker:把一组分析器变成子命令,由go命令可扩展的增量构建系统运行,类似go build中的编译器。它是go fix和go vet的基础。nogo:为 Bazel、Blaze 等其他构建系统提供类似的驱动程序。singlechecker:把单个分析器变成独立命令,加载、解析并对一组包(可能是整个程序)进行类型检查,然后执行分析。我们经常用它在模块镜像proxy.golang.org的语料上开展临时实验与测量。multichecker:对一组分析器执行类似工作,并提供包含多种功能的命令行界面。gopls:为 VS Code 等编辑器提供支持的语言服务器。每次输入后,它都可以实时给出分析器诊断。staticcheck使用的高度可配置驱动程序。Staticcheck 本身也提供大量可以由其他驱动程序运行的分析器。Tricorder:Google 单体仓库使用的批量静态分析流水线,并与代码审查系统集成。gopls的 MCP 服务器:让基于大语言模型的编程代理获取诊断,提供更可靠的约束。analysistest:分析框架的测试工具。
框架的一项优势是可以定义辅助分析器。它们自己不报告诊断,也不建议修正,而是计算可能被多个分析器使用的中间数据结构,从而分摊构建这些结构的成本。例如,控制流图、函数体的 SSA 表示,以及用于高效遍历 AST 的数据结构。
另一项优势是支持跨包推断。分析器可以在函数或其他符号上附加一个“事实”,让分析函数体时得出的信息用于之后对函数调用的分析,即使调用位于另一个包,或后续分析发生在另一个进程中。这让可扩展的过程间分析更容易实现。例如,printf 检查器能识别 log.Printf 实际上是 fmt.Printf 的包装函数,因此知道应当以类似方式检查 log.Printf 的调用。这种推断可以逐层延伸,因此再包装 log.Printf 的函数也会得到检查。Uber 的 nilaway 是大量使用“事实”的一个分析器,它报告可能因解引用 nil 指针造成的错误。
go fix 中的“分离分析”类似 go build 的分离编译:编译器从依赖图底部开始构建包,并把类型信息传递给导入它们的包;分析框架也从依赖图底部向上工作,把事实和类型传递给导入这些包的上层包。
2019 年开始开发 Go 语言服务器 gopls 时,我们加入了分析器在报告诊断时建议修正的能力。例如,printf 分析器会建议把 fmt.Printf(msg) 改成 fmt.Printf("%s", msg),避免动态字符串 msg 中包含 % 时产生格式化错误。这套机制后来成为 gopls 许多快速修复和重构功能的基础。
go vet 不断演进时,go fix 却仍停留在 Go 兼容性承诺建立之前的状态。那时,早期用户用它维护代码,以应对语言和标准库快速、偶尔不兼容的变化。
Go 1.26 把 Go 分析框架引入 go fix。go vet 与 go fix 已经汇合,实现几乎完全相同,区别主要在于选择算法的标准,以及如何处理诊断结果。go vet 分析器需要以较低误报率发现可能的错误,并把诊断报告给用户;go fix 分析器则需要生成可以安全应用、不会损害正确性、性能或风格的修正。它的诊断可能不显示,但修正会直接应用。除了侧重点不同,开发修正器与开发检查器没有本质区别。
改进分析基础设施
随着 go vet 和 go fix 的分析器数量增加,我们持续改进基础设施,既提升每个分析器的性能,也让新的分析器更容易编写。
例如,大多数分析器先遍历包中每个文件的语法树,寻找某类节点,例如 range 语句或函数字面量。现有的 inspector 包会预先为一次完整遍历计算紧凑索引,使后续遍历可以快速跳过不包含目标节点的子树。最近,我们加入了 Cursor 数据类型,让节点之间可以像 HTML DOM 一样,灵活、高效地向上、下、左、右移动,从而方便表达“找出循环体中第一条语句为 go 语句的所有位置”这样的查询:
var curFile inspector.Cursor = ...
// Find each go statement that is the first statement of a loop body.
for curGo := range curFile.Preorder((*ast.GoStmt)(nil)) {
kind, index := curGo.ParentEdge()
if kind == edge.BlockStmt_List && index == 0 {
switch curGo.Parent().ParentEdgeKind() {
case edge.ForStmt_Body, edge.RangeStmt_Body:
...
}
}
}
许多分析器首先寻找对特定函数的调用,例如 fmt.Printf。函数调用是 Go 代码中最常见的表达式之一,因此逐个检查所有调用是不是 fmt.Printf 效率并不好。更有效的方法是预先建立符号引用索引,typeindex 及其辅助分析器负责完成这一工作。然后就可以直接枚举 fmt.Printf 的调用,让成本与调用次数成正比,而非与包大小成正比。对 hostport 这样寻找较少出现的符号 net.Dial 的分析器,原文给出的案例中,这种方法可以快约 1,000 倍。
过去一年还做了以下基础设施改进:
- 提供可供分析器查询的标准库依赖图,避免引入循环导入。例如,不能在一个本身被
strings导入的包中加入对strings.Cut的调用。 - 支持根据所在模块的
go.mod和构建标签,查询文件的有效 Go 版本,避免分析器加入“太新”的特性。 - 提供更丰富的重构基础操作,例如“删除这条语句”,正确处理相邻注释和其他棘手边界情况。
虽然已经取得许多进展,仍然还有很多工作。修正器的逻辑很难在所有情况下都正确。用户可能只做粗略审查就应用数百项修正,因此即使是少见的边界情况,修正器也必须可靠。一个例子是:我们编写了分析器,把 append([]string{}, slice...) 替换成更清楚的 slices.Clone(slice),后来才发现 slice 为空时,Clone 的结果是 nil。这种细微行为变化在少数情况下会造成缺陷,所以我们不得不把该分析器排除出 go fix 的工具集合。我的 GopherCon 演讲中还有其他例子。
更好的文档可以减轻分析器作者遇到的部分困难,无论文档面向人还是大语言模型,尤其应包含需要考虑和测试的意外边界情况清单。类似 Staticcheck、Tree Sitter 的语法树模式匹配引擎,可以简化高效定位需要修正位置的繁琐工作。更丰富的精确修正运算可以帮助避免常见错误;更好的测试工具可以检查修正是否破坏构建、是否保持目标代码的动态性质。这些都在我们的路线图中。
“自助式”范式
更根本地说,我们在 2026 年开始把注意力转向“自助式”范式。
前面介绍的 newexpr 是典型的现代化分析器:为某个特性专门设计算法。这种方式适合语言和标准库特性,却不能很好地更新第三方包的使用方式。你当然可以为自己的公开 API 编写分析器并在项目里运行,但没有自动机制让 API 的用户也运行它。除非你的 API 在 Go 生态里极其普及,否则这个分析器通常不会进入 gopls 或 go vet 的默认集合。即使很普及,也要经过代码审查与批准,并等待下一个版本。
在自助式范式下,Go 程序员可以为自己的 API 定义现代化规则,让用户直接应用,避开目前集中管理流程中的瓶颈。Go 社区和全球代码总量的增长速度远高于我们团队审查分析器贡献的能力,因此这尤为重要。
Go 1.26 的 go fix 已经预览了这一范式的第一项成果:由注解驱动、在源码层面运行的内联器,后续文章会介绍它。原文发表时,作者计划在接下来的一年探索另外两个方向。
第一,研究从源码树动态加载现代化分析器,并在 gopls 或 go fix 中安全执行它们的可能性。这样,一个提供 SQL 数据库 API 的包,也可以提供检查器,发现 API 使用错误,例如 SQL 注入漏洞或没有处理关键错误。项目维护者也可以用同一机制表达内部维护规则,例如避免调用有问题的函数,或在关键代码中实施更严格的编程要求。
第二,许多现有检查器可以概括为“做完 Y 之后,别忘了 X”,例如打开文件后关闭、创建 context 后取消、加锁后解锁、yield 返回 false 后退出迭代循环。它们的共同点是:要求所有执行路径都保持某些不变条件。我们计划研究这些控制流检查器的推广与统一,让 Go 程序员只需为自己的代码添加注解,就能把它们用于新领域,而不必编写复杂分析逻辑。
我们希望这些新工具能减少 Go 项目维护工作,也帮助你更早了解并使用新的特性。欢迎在项目中尝试 go fix,报告遇到的问题,并分享对新现代化分析器、修正器、检查器或自助式静态分析方法的想法。
相关文档与链接
- second section
- “self-service”
- generated files
- generics
- maps.Keys
- talk
- dozens of analyzers
- gopls
- proposal
- proposals
- json.Marshal
- protocol buffers
- proto.Int64
- proto.String
- newexpr
- go 1.26 directive
- build constraint
- deadcode
- QF1012
- staticcheck
- plan
- Go analysis framework
- unitchecker
- nogo
- singlechecker
- proxy.golang.org
- multichecker
- language server
- Tricorder
- MCP server
- analysistest
- control-flow graphs
- SSA representation
- optimized AST navigation
- fact
- Uber’s nilaway
- fix
- Go compatibility promise
- vet analyzers
- fix analyzers
- inspector
- Cursor
- typeindex
- helper
- hostport
- 1,000× faster
- talk
- that modernizer
- staticcheck
- Tree Sitter
- a follow-up post
- dynamically loading
- report
来源与许可
原文:Alan Donovan,Using go fix to modernize Go code,2026 年 2 月 17 日。此中文版本翻译正文,保留代码与分析示意图;未来版本计划按原文发表时态表述。 阅读原文。
Go 网站正文按 CC BY 4.0 许可使用;此版本是中文改编,原作者并未为本站背书。网站许可声明 · CC BY 4.0
代码的 BSD 许可与版权声明
Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.











暂无评论内容