关于 go-refactoring
go-refactoring 是面向 Go 开发者和编码 Agent 的重构工作流技能,强调每次只做小幅、可验证且不改变行为的调整,并提供风险检查与回退指引。
主要功能
- 01按风险划分重构步骤
- 02设置构建与测试检查点
- 03安全重命名与代码提取
- 04包和 API 迁移模式
- 05机械化改写工具指引
适用场景
- 01规划包的拆分
- 02安全重命名 Go 符号
- 03调整已导出的 API
- 04逐步替换子系统
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-refactoring安装后在 Claude Code 中输入/go-refactoring确认已加载。
附带文件references/bulk-rewrites.md
详细介绍
Go 重构工作流
重构改变的是结构,而不是行为。完成的定义是机械性的:每一步前后都通过相同的测试,并且每一步都足够小,可以单独回滚。
按需加载的详细参考资料:
references/bulk-rewrites.md—gofmt -r、//go:fix inline、eg、gopatch和自定义分析器,以及之后要运行的验证。
只有在下方的摘要不够用时,才阅读参考文件。
0. 风险、检查点和不该重构的情况
| 风险 | 转换 | 开始前 |
|---|---|---|
| 低 | gopls 重命名、提取/内联变量、整理导入 | 构建和测试通过 |
| 中 | 提取函数或方法、跨包内联调用、添加/删除参数 | 测试覆盖每个被修改的调用点 |
| 高 | 跨包移动类型、拆分或合并包、打破循环依赖、更改导出 API | 完整的安全网,以及开发者的签字确认 |
任何跨包移动、导出 API 更改、删除,或对没有测试的代码进行修改前,都应暂停并征求开发者批准。如果代码很稳定且计划中没有任何改动会涉及它、代码至关重要但未经测试,或没有任何具体的后续改动会因此变得更容易,就应质疑这次重构是否必要。
1. 循环
- 基线: 在动手前,
go build ./... && go test ./...必须通过。如果目标代码周围缺少测试,先编写特征测试——即使当前行为看起来不正确,也要固定住它。 - 从下方目录中选择一项转换。
- 验证: 构建 + 测试 +
go vet ./...。 - 提交。 绝不要把重构提交与行为更改混在一起——审查者可以快速浏览
refactor:提交,但必须仔细审查混合提交。 - 重复。
如果第 3 步失败,且一分钟内看不出明显的修复方法,就回滚这一步,而不是调试只完成一半的转换。
2. 重命名——交给工具处理
# Preferred: gopls (understands types, interfaces, embedding)
gopls rename -w internal/service/user.go:#offset newName
# Module path or package import path changes:
# update go.mod, then rewrite imports mechanically
find . -name '*.go' -exec sed -i 's|github.com/acme/old|github.com/acme/new|g' {} +
go build ./... # the compiler is the reviewer
重命名后,应在结构体标签、模板、SQL 和反射会读取的所有内容中搜索旧名称——gopls 会检查编译,但不会检查这些内容。重命名字段会导致它与 json/db 标签悄然不同步。
绝不要在没有弃用周期的情况下重命名已发布库的导出标识符:添加新名称,将旧名称标记为 // Deprecated: use NewName.,并在下一个主版本中删除旧名称。
3. 提取包
以编译器逐步检查的方式,将代码从一个庞大的包中移出:
- 创建新包;移动一个类型及其方法(使用
gopls或剪切/粘贴),其余内容保持不动。 - 在旧包中添加类型别名,确保不会破坏任何内容:
type User = user.User(使用别名,即=,而不是定义)。 - 构建。分批将导入方迁移到新路径;每批完成后都构建。
- 没有导入方后,删除这些别名。
这样,即使调用方数量任意庞大,每次提交也都能保持通过。
4. 打破循环依赖
包 a → b 和 b → a 无法编译;近似循环依赖通常会表现为庞大的包。按优先顺序,有三种解决方法:
- 提取共享核心:
a和b实际上都依赖某个类型——将它移到第三个不导入任何包的包c中。 - 通过接口反转依赖: 如果
store会回调service,就在store中(使用方一侧)定义回调接口,并由service实现它。编译时依赖方向就会反转。 - 合并: 如果两个包无法在没有彼此的情况下描述,它们原本就是同一个包。
5. 安全地更改函数签名
对于调用方很多的导出函数:
// Step 1 — add the new form alongside the old
func (s *Service) ProcessCtx(ctx context.Context, id string) error { ... }
// Step 2 — old form delegates; mark deprecated
// Deprecated: use ProcessCtx.
func (s *Service) Process(id string) error {
return s.ProcessCtx(context.Background(), id)
}
// Step 3 — migrate callers batch by batch, building each batch
// Step 4 — delete the old form (same module) or keep until next major (library)
在 Go 1.26+ 中,可在弃用包装函数上方添加 //go:fix inline,这样第 3 步就变为 go fix ./...——现在可用于你的模块,之后每个下游模块运行 go fix 时也会生效。
在单个模块内,如果编译器可以帮你找到所有调用方,优先采用原子式签名更改:先修改签名,再逐一解决构建错误——编译器会列出你的 TODO 清单。
6. 子系统的绞杀式迁移
替换一个大到无法通过单个 PR 完成的子系统(旧存储、旧客户端):
- 定义调用方实际需要的消费者侧接口。
- 让旧实现满足该接口;将调用方接入该接口。
- 在同一接口后构建新实现;用一套共享的一致性测试同时测试两种实现。
- 在组合根中切换接线(只需一行、一个提交,可轻松还原)。如有风险,可使用功能标志。
- 在单独的提交中删除旧实现。
7. 批量机械式重写
如果同一项变更需要在许多位置实施,就将其编码一次,再交给工具应用:对语法表达式使用 gofmt -r;对自有 API 的调用方使用 //go:fix inline + go fix;对需要类型感知的表达式重写使用 eg;对语句和导入项使用 gopatch;对会重复运行的迁移使用 go/analysis 修复器。工具生成的重写可供审查,也可重复运行;四十处手动修改则两者皆不可。示例:references/bulk-rewrites.md。
8. 什么不属于重构
- “既然我在这儿”顺手修复的 Bug——应在之前或之后单独提交。
- 对位置字面量所用结构体字段重新排序、更改调用方会匹配的导出错误字符串、更改 JSON 标签——这些都是披着重构外衣的行为变更。
- 没有测试的重写。如果无法先明确行为,你就不是在重构,而是在赌博。
验证清单
- 首次变更前,基线构建和测试均通过
- 在覆盖不足的地方添加特征测试
- 每个提交只进行一种变换;
refactor:提交不包含行为变更 - 使用 gopls/编译器完成重命名,而不是对标识符进行查找替换
- 提取包时使用类型别名,确保迁移过程中始终通过测试
- 不引入新的依赖环(
go build ./...可验证这一点) - 公共 API 变更遵循弃用周期
- 每个提交都通过
go vet ./...和完整测试套件,而不只是最后一个提交 - git log 展示一系列安全、可还原的步骤
- 高风险步骤开始前须经开发者批准
- 每次重命名后都搜索结构体标签、模板和 SQL