AI 中文社区(简称 AI 中文社),是国内学习交流AI人工智能技术的中文社区网站,这里可获取及贡献任何AI人工智能技术,我们追求自由、简洁、纯粹、分享的多元化人工智能社区。

Go 1.26 可以使用 go fix 指令修复老旧 Go 代码更加现代化

Go · 杰作 15小时前发布 · 浏览23次 · 点赞0次 · 收藏0次

作者:Alan Donovan 艾伦·多诺万 2026年2月17日

本月发布的 Go 1.26 包含了一个完全重写的 go fix 子命令。go fix 运用一套算法来发现优化代码的机会,通常是利用语言与标准库中更现代化的特性。在这篇文章中,我们会先教你如何使用 go fix 现代化你的 Go 代码库。然后在第二部分深入其底层基础设施以及它的演进方向。最后,我们会介绍“自助式”分析工具的理念,帮助模块维护者和组织将自己的规范与最佳实践固化成工具。

运行 go fix

go buildgo vet 一样,go fix 命令接受一组表示包的路径模式。下面这条命令会修复当前目录下的所有包:

$ go fix ./...

执行成功时,它会静默更新你的源文件。它会忽略所有对自动生成文件的修改,因为这类文件正确的修复方式应该是修改生成器本身的逻辑。我们建议你每次将构建工具链升级到新版本 Go 时,都在项目中运行一次 go fix。由于该命令可能会修改数百个文件,建议从一个干净的 git 状态开始,这样改动就只来自 go fix;你的代码审阅者会为此感谢你。

如果想预览命令会做出哪些修改,可以使用 -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

输出中会列出已注册的分析器,例如:

  • any:将 interface{} 替换为 any

  • buildtag:检查 //go:build// +build 指令

  • fmtappendf:将 []byte(fmt.Sprintf) 替换为 fmt.Appendf

  • forvar:移除循环变量多余的重复声明

  • hostport:检查传给 net.Dial 的地址格式

  • inline:根据 go:fix inline 注释应用修复

  • mapsloop:将显式遍历 map 改为调用 maps 包函数

  • minmax:将 if/else 语句替换为 minmax 调用

    指定某个分析器名称可以查看完整文档:

$ go tool fix help forvar

默认情况下,go fix 会运行所有分析器。修复大型项目时,如果你把产出改动最多的分析器分开应用,能减轻代码审阅的负担。如果只想启用特定分析器,可以用对应名称的 flag。例如只运行 any 修复器,就用 -any。反之,如果要运行除了某个分析器之外的所有分析器,可以将 flag 设为 false,例如 -any=false

go buildgo vet 一样,每次运行 go fix 只分析特定的构建配置。如果你的项目大量使用针对不同 CPU 或平台的标记文件,你可能需要用不同的 GOARCHGOOS 多运行几次以获得更完整的覆盖:

$ 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月,在 LLM 编码助手疯狂普及期间,我们发现这类工具生成的 Go 代码风格往往和训练数据里的大量旧代码相似,即便有更新、更好的写法也不会用。更不明显的是,即使明确要求“始终使用 Go 1.25 最新写法”,这些工具也常常拒绝使用新方式。某些情况下,就算明确要求使用某个新特性,模型也会否认它的存在。

为了确保未来的模型能在最新写法上训练,我们需要保证这些最新写法出现在训练数据里,也就是全球开源 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

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 表示可选值的代码中尤其方便,比如 json、Protobuf 等序列化库。以前人们经常需要写辅助函数:

func newInt(x int) *int { return &x }

Go 1.26 让所有这类辅助函数都不再需要:

Attempts: new(10),

为了帮你用上这个特性,go fix 现在提供了 newexpr 修复器,它能识别 newInt 这类“类 new”函数,并建议将函数体改为 return new(x),同时把所有调用点(无论在同一个包还是导入包中)直接改为 new(expr)

为避免过早使用新特性,现代化修复器只会在满足最低 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

不同分析器之间也可能产生联动。比如循环中反复拼接字符串会导致平方级时间复杂度,stringsbuilder 修复器会建议使用 strings.Builder;应用后,另一个分析器又会发现可以把 WriteStringSprintf 合并成 fmt.Fprintf,提供第二次修复。

因此,值得多次运行 go fix 直到不再产生改动;通常运行两次就够了。

修复合并与冲突

单次运行 go fix 可能会在同一个源文件中应用数十处修复。所有修复在逻辑上都是独立的,类似于拥有同一个父提交的一组 git 提交。go fix 使用简单的三路合并算法按顺序合并这些修复。如果某个修复与已累积的编辑冲突,该修复会被丢弃,工具会发出警告,提示部分修复被跳过,建议重新运行。

这种方式能可靠检测出由重叠编辑导致的语法冲突,但另一类冲突也可能出现:语义冲突——两处改动文本上独立,但含义不兼容。例如两个修复各自删除局部变量的倒数第二次使用,单独看都没问题,但一起应用会导致变量未使用,在 Go 中这是编译错误。

类似的语义冲突还会出现在导入变得未使用时。由于这种情况非常常见,go fix 会在最后自动检测并删除未使用的导入。

语义冲突相对少见,而且通常会表现为编译错误,很难被忽略。缺点是发生后需要在运行 go fix 之后做一些手工处理。

下面我们深入这些工具背后的基础设施。

Go 分析框架

从 Go 早期开始,go 命令就有两个用于静态分析的子命令:go vetgo fix,各自拥有一套算法:“检查器”和“修复器”。检查器报告代码中可能的错误,修复器则安全地编辑代码来修复错误或以更好的方式表达逻辑。

2017年,我们重新设计了当时单体化的 go vet,将检查器算法(现在称为“分析器”)与运行它们的“驱动程序”分离;结果就是 Go 分析框架。这种分离让一个分析器只需编写一次,就可以在各种环境的驱动中运行,例如:

  • unitchecker:将一组分析器变成可被 go 命令增量构建系统运行的子命令

  • nogo:适用于 Bazel、Blaze 等构建系统

  • singlechecker:将分析器变成独立命令,用于实验和测量

  • multichecker:支持一组分析器的瑞士军刀 CLI

  • gopls:编辑器中的语言服务,提供实时诊断

  • staticcheck 使用的高度可配置驱动

  • Tricorder:Google 单仓使用的批量静态分析流水线

  • gopls MCP 服务器:为 LLM 智能体提供诊断

  • analysistest:分析框架的测试工具

框架的一个好处是可以表达辅助分析器,它们本身不报告问题或提供修复,而是计算中间数据结构供许多其他分析器使用,例如控制流图、SSA 形式、优化后的 AST 导航结构。

image.png

另一个好处是支持跨包推导。分析器可以给函数或符号附加一个“事实”,以便在分析调用点时使用,即使调用在另一个包或另一个进程中。这让大规模过程间分析变得简单。例如 printf 检查器可以识别 log.Printffmt.Printf 的包装,从而正确检查参数。

2019年开发 gopls 时,我们为分析器增加了在报告诊断时提供修复的能力。这一机制成为了 gopls 大量快速修复与重构功能的基础。

go vet 不断发展的同时,go fix 却长期停滞。Go 1.26 终于将 Go 分析框架带入了 go fix。现在 go vetgo fix 在实现上几乎完全相同,唯一区别是使用算法的标准和对诊断结果的处理:

  • go vet 要求低误报率,只报告问题

  • go fix 要求修复安全无倒退,直接应用修改

改进分析基础设施

随着 go vetgo fix 中分析器数量不断增长,我们持续投入基础设施建设,提升每个分析器的性能,并让新分析器更容易编写。

例如大多数分析器都需要遍历语法树寻找特定节点。现有的 inspector 包通过预先计算索引让扫描更高效。最近我们扩展了 Cursor 类型,支持在节点之间上下左右灵活导航,让表达“找到循环体第一条语句是 go 语句”这类查询变得简单高效。

很多分析器需要查找特定函数调用(如 fmt.Printf)。typeindex 及其辅助分析器会预先计算符号引用索引,让查找直接枚举调用点,成本与调用数成正比而非包大小,对不常用函数(如 net.Dial)的分析速度可提升 1000 倍。

过去一年的其他基础改进包括:

  • 标准库依赖图,避免引入导入循环

  • 查询文件有效 Go 版本的支持,避免插入过新的特性

  • 更丰富的重构原语库,正确处理注释与边界情况

    我们已经取得很大进展,但仍有很多工作要做。修复器逻辑在边界情况下必须绝对正确,因为用户可能只做粗略检查就应用上百处修复。我们会继续完善文档、测试、模式匹配引擎、操作符库和测试工具。

“自助式”理念

更根本的是,我们在 2026 年将注意力转向**“自助式”(self-service)理念**。

前面看到的 newexpr 是典型的现代化修复器:为特定功能定制的算法。这种定制模型对语言和标准库很适合,但无法帮助更新第三方包的使用。虽然你可以为自己的公共 API 编写现代化修复器,但没有自动化方式让 API 用户也去运行它。

在自助式理念下,Go 开发者可以为自己的 API 定义现代化改造,让用户无需经过当前集中式模式的各种瓶颈就能应用。这在 Go 社区和代码库增长远快于团队审核贡献速度的今天尤为重要。

Go 1.26 中的 go fix 已经包含这一新理念的首个成果:基于注解的源码级内联器,我们将在下周的配套博客文章中详细介绍。未来一年,我们计划在这个理念下再探索两种方向:

第一,探索从源码树动态加载现代化修复器并安全执行的可能性,无论是在 gopls 还是 go fix 中。这样,提供 SQL 数据库 API 的包可以额外提供检查器,检查 SQL 注入、错误处理等问题。项目维护者也可以用这种机制固化内部规范,比如禁止调用某些有问题的函数,或在关键代码路径上强制执行更严格的编码规范。

第二,很多现有检查器都可以概括为“在 Y 之后不要忘记 X”,例如“打开文件后记得关闭”“创建上下文后记得取消”“锁定互斥锁后记得解锁”。它们的共同点是在所有执行路径上强制某种不变式。我们计划探索对这类控制流检查器的通用化与统一化,让 Go 开发者只需简单注解自己的代码,就能轻松应用到新领域,无需编写复杂分析逻辑。

我们希望这些新工具能减轻你维护 Go 项目的负担,帮助你更快了解并受益于新特性。欢迎在你的项目中试用 go fix 并反馈问题,也欢迎分享你对新现代化修复器、检查器或自助式静态分析方法的想法。

官方原文:https://golang.google.cn/blog/gofix

Go 1.26 可以使用 go fix 指令修复老旧 Go 代码更加现代化 - Go - 话题 - AI 中文社区
点赞(0) 收藏(0)
0条评论
现在评论,你将成小区里最靓的仔^_^
评论
游客
游客
登录后再评论
  • 一字一句需斟酌,一言一语显风范。
  • 评论消耗5积分,点赞、收藏消耗3积分。