Go 项目清依赖别用正则,molt 认得 go.mod 的 replace
在公司推行 Go 语言规范时,我发现很多老项目里堆满了 github.com/pkg/errors 或者 golang.org/x/exp/slices。其实 Go 1.13 之后有了 %w,1.21 之后有了原生的 slices 和 maps 包,这些第三方依赖早就不需要了。但问题是,没人敢随便删,因为你得确认代码里调用的函数行为和标准库完全一致,否则这就是个巨大的 Regression 风险。
之前我想过写个脚本跑正则替换,但正则处理不了 replace 指令带来的路径重定向。最近研究了 molt 这个工具,它最硬核的地方在于它本身实现了「零依赖」——它的 go.mod 里一个 require 块都没有,完全靠 Go 标准库自带的解析器跑。
为什么不能简单地用正则替换导入路径
很多人以为只要把 import "golang.org/x/exp/slices" 换成 import "slices" 就算搞定了,但这里有个坑:签名一致不代表行为一致。
如果项目里用了 replace 指令,某个第三方包可能被指向了一个私有的 fork 版本,那个 fork 里的 SortFunc 行为可能被改过了。如果你强行替换成标准库,编译能过,但运行时的逻辑可能直接崩掉。
molt 的处理逻辑是利用 go/parser 和 go/ast 做静态分析。它不是在搜字符串,而是在解析抽象语法树(AST)。
// 它是这样定位哪些包被使用了的
af, _ := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
for _, spec := range af.Imports {
// 记录本地名称与导入路径的映射
}
ast.Inspect(af, func(n ast.Node) bool {
if sel, ok := n.(*ast.SelectorExpr); ok {
if id, ok := sel.X.(*ast.Ident); ok {
// 这里的 id.Name 就是包名,sel.Sel.Name 就是函数名
// 比如 "uuid.New",它能精准捕捉到谁在调用 New
}
}
return true
})
这种做法比正则稳得多,因为它能区分出 uuid.New 是调用了标准库还是某个第三方包,从而决定是否可以安全地重写。
实操中的性能与限制
我在一个中型项目(约 200 个 .go 文件)上跑了一下,全量扫描耗时在 1 秒以内,几乎感知不到。因为它跳过了 ObjectResolution(对象解析),只做语法层面的扫描,所以速度极快。
不过在使用时有几个点需要注意:
- 版本依赖: 只有当你升级到 Go 1.21+ 之后,使用这个工具剔除
x/exp系列包才有意义。 - 风险点: 虽然它能证明签名一致,但如果你的代码依赖于某些第三方包的非公开行为(虽然概率低),替换后仍需跑一遍全量集成测试。
- 运行成本: 因为它是零依赖的单二进制文件,不需要
go mod download任何东西,直接跑就行,没有任何环境配置成本。
避坑指南:别把 x/tools 当成标准库
很多开发者在写静态分析工具时习惯性地依赖 golang.org/x/tools/go/packages,这东西确实好用,但它不是标准库。如果你追求极致的轻量化,或者在构建一个对依赖极其敏感的底层工具,你应该像 molt 这样直接用 go/ast 和 go/token。
一个真实的教训是,如果你在 go.mod 追求零依赖,就得接受不能使用 x/tools 带来的便利,得自己处理 ast.SelectorExpr 这种底层结构。

这逻辑绝了,要是能把那些静默变更直接标红,我就不用盯着 500 行 diff 找 Bug 了...