mcpField
G
go-refactoring

go-refactoring

github.com/eduardo-sl开发83100 次安装2 周前更新
代码仓库

关于 go-refactoring

go-refactoring 是面向 Go 开发者和编码 Agent 的重构工作流技能,强调每次只做小幅、可验证且不改变行为的调整,并提供风险检查与回退指引。

主要功能

  1. 01按风险划分重构步骤
  2. 02设置构建与测试检查点
  3. 03安全重命名与代码提取
  4. 04包和 API 迁移模式
  5. 05机械化改写工具指引

适用场景

  1. 01规划包的拆分
  2. 02安全重命名 Go 符号
  3. 03调整已导出的 API
  4. 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. 循环

  1. 基线: 在动手前,go build ./... && go test ./... 必须通过。如果目标代码周围缺少测试,先编写特征测试——即使当前行为看起来不正确,也要固定住它。
  2. 从下方目录中选择一项转换。
  3. 验证: 构建 + 测试 + go vet ./...。
  4. 提交。 绝不要把重构提交与行为更改混在一起——审查者可以快速浏览 refactor: 提交,但必须仔细审查混合提交。
  5. 重复。

如果第 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. 提取包

以编译器逐步检查的方式,将代码从一个庞大的包中移出:

  1. 创建新包;移动一个类型及其方法(使用 gopls 或剪切/粘贴),其余内容保持不动。
  2. 在旧包中添加类型别名,确保不会破坏任何内容: type User = user.User(使用别名,即 =,而不是定义)。
  3. 构建。分批将导入方迁移到新路径;每批完成后都构建。
  4. 没有导入方后,删除这些别名。

这样,即使调用方数量任意庞大,每次提交也都能保持通过。

4. 打破循环依赖

包 a → b 和 b → a 无法编译;近似循环依赖通常会表现为庞大的包。按优先顺序,有三种解决方法:

  1. 提取共享核心: a 和 b 实际上都依赖某个类型——将它移到第三个不导入任何包的包 c 中。
  2. 通过接口反转依赖: 如果 store 会回调 service,就在 store 中(使用方一侧)定义回调接口,并由 service 实现它。编译时依赖方向就会反转。
  3. 合并: 如果两个包无法在没有彼此的情况下描述,它们原本就是同一个包。

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 完成的子系统(旧存储、旧客户端):

  1. 定义调用方实际需要的消费者侧接口。
  2. 让旧实现满足该接口;将调用方接入该接口。
  3. 在同一接口后构建新实现;用一套共享的一致性测试同时测试两种实现。
  4. 在组合根中切换接线(只需一行、一个提交,可轻松还原)。如有风险,可使用功能标志。
  5. 在单独的提交中删除旧实现。

7. 批量机械式重写

如果同一项变更需要在许多位置实施,就将其编码一次,再交给工具应用:对语法表达式使用 gofmt -r;对自有 API 的调用方使用 //go:fix inline + go fix;对需要类型感知的表达式重写使用 eg;对语句和导入项使用 gopatch;对会重复运行的迁移使用 go/analysis 修复器。工具生成的重写可供审查,也可重复运行;四十处手动修改则两者皆不可。示例:references/bulk-rewrites.md。

8. 什么不属于重构

  • “既然我在这儿”顺手修复的 Bug——应在之前或之后单独提交。
  • 对位置字面量所用结构体字段重新排序、更改调用方会匹配的导出错误字符串、更改 JSON 标签——这些都是披着重构外衣的行为变更。
  • 没有测试的重写。如果无法先明确行为,你就不是在重构,而是在赌博。

验证清单

  1. 首次变更前,基线构建和测试均通过
  2. 在覆盖不足的地方添加特征测试
  3. 每个提交只进行一种变换;refactor: 提交不包含行为变更
  4. 使用 gopls/编译器完成重命名,而不是对标识符进行查找替换
  5. 提取包时使用类型别名,确保迁移过程中始终通过测试
  6. 不引入新的依赖环(go build ./... 可验证这一点)
  7. 公共 API 变更遵循弃用周期
  8. 每个提交都通过 go vet ./... 和完整测试套件,而不只是最后一个提交
  9. git log 展示一系列安全、可还原的步骤
  10. 高风险步骤开始前须经开发者批准
  11. 每次重命名后都搜索结构体标签、模板和 SQL

评论

支持 Markdown