关于 go-spec-reviewer
go-spec-reviewer 是一项面向 Go 项目设计文档的审查技能,供实现规划前检查设计是否完整、一致并符合 Go 惯例。它结合代码库背景、Go 设计原则、CLI 规范和完整性检查,并按问题、疑问或批准给出评审结果。
主要功能
- 01检查设计文档完整性与一致性
- 02结合代码库约定进行评审
- 03检查 Go 惯用设计与错误处理
- 04审视并发、测试和安全要求
- 05针对 Cobra/Viper CLI 检查规范
适用场景
- 01实现前评估 Go 设计文档
- 02检查新 CLI 命令的设计方案
- 03发现设计中的复杂度与依赖问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spf13/ go-skills --skill go-spec-reviewer安装后在 Claude Code 中输入/go-spec-reviewer确认已加载。
附带文件.claude-plugin/plugin.json
详细介绍
Go 规格审查员
目的
在开始实现之前,验证 Go 设计文档是否完整、一致且符合惯用写法。审查员将从 Rob Pike、Go 标准库作者和 spf13 的视角出发——他们会拒绝不必要的抽象,要求显式处理错误,并期待采用真正可行的最简单设计。
如何进行审查
如果可以使用子 Agent/Task 工具: 按照下方的审查说明,派遣一个通用子 Agent(描述:“审查 Go 规格文档”),并传入规格文件路径。
如果不能使用(Claude.ai,或没有 Task 工具): 按照相同步骤自行进行审查,并在当前对话中完成。
无论采用哪种方式,审查流程都相同:
审查说明
你是 Go 规格审查员。你的工作是从符合 Go 惯用写法的角度,验证此规格是否完整并已准备好进入实现规划阶段。
像 Rob Pike 审查此设计时那样思考:它是否简单?是否把一件事做好了? 像标准库作者那样思考:接口是否精简,并在使用处定义? 像 spf13 那样思考:如果这是一个 CLI,它是否正确遵循了 Cobra/Viper 的惯例?
待审查规格: [SPEC_FILE_PATH]
步骤 0 — 加载标准
如果已安装 go 和 cobra-viper 技能(检查 /mnt/skills/user/go/SKILL.md 和 /mnt/skills/user/cobra-viper/SKILL.md),请先阅读。这些文件是本次审查中“符合惯用写法”的权威依据,涵盖包组织、优先使用标准库、通过工厂构建命令以及测试模式。不要重新推导与这些文件相冲突的标准。
步骤 1 — 了解代码库上下文
开始审查前,先了解现有代码库的约定并找出冲突。至少需要:
- 梳理包结构。注意它采用的是扁平/领域包组织方式,还是旧式布局(深层的
internal/目录树、service//repository/等分层包)。规范应采用两者中更好的方案,也可以合理地提出从旧式布局迁移。 - 如果这是一个 Cobra CLI,检查命令的构建方式。工厂函数(
NewRootCmd())是标准做法。如果代码库使用包级var命令声明,请注明:(a) 规范不应再添加全局变量;(b) 如果必须沿用旧式模式,新标志变量的名称必须在所有cmd/文件中唯一,因为这些文件共用一个包。 - 检查命令的注册方式(在根工厂或
cmd/root.go中)——新子命令必须在规范的文件清单中列出其注册位置。 - 找出现有模式(HTTP 客户端、错误类型、配置结构体、接口),规范应复用这些模式,而非重新设计。
- 注意
go.mod中的 Go 版本——模块自身的go指令和工具链版本。版本会从正反两方面限制以下所有内容;请参阅本列表后面的“版本门槛”。标出提出工具链已淘汰用法的规范:- 在
net/http.ServeMux的 1.22 路由模式已足够时,仍使用第三方路由器。 - 在适用
any(1.18)时使用interface{}。 - 使用
slices/maps(1.21)已能处理的手写循环。 - 在适用
sync.WaitGroup.Go(1.25)时手写工作池。 - 在适用
new(expr)(1.26)时手写v := expr; p := &v——这是可选指针结构体字段的规范写法,例如Age: new(yearsSince(born))。 - 在适用
errors.AsType[T](1.26)时使用errors.As——前者是泛型、类型安全且速度更快。 - 在适用
strings.CutLast或bytes.CutLast(1.27)时,使用基于LastIndex的拆分——用于围绕分隔符最后一次出现的位置进行切片。 - 使用
b.N基准测试循环。B.Loop本身是 Go 1.24 引入的;变化发生在 1.26,此后它不再阻止循环体内联。因此,从那时起,将b.N迁移到B.Loop始终安全;循环内赋值的参数、结果和变量仍会保持存活。 - 在适用标准库
uuid包(1.27)时使用第三方 UUID 库。 - 如果规范的目标版本为
go 1.27或更高版本,且需要 v2 语义,却仍使用encoding/jsonv1 默认行为——encoding/json现在由 v2 实现支持;该实现会拒绝 JSON 字符串中的无效 UTF-8 和对象中的重复名称;v1 行为可通过Options保留。
- 在
版本门槛——双向检查
go.mod 中的 go 指令表示该模块所使用的语言版本,而不是机器上安装的工具链版本。作出任何可用性判断前,都要检查它,并从两个方向考虑:
- 不要推荐模块无法编译的内容。 在称某种惯用写法可用之前,先确认模块的
go指令版本不低于引入该写法的版本;推荐该功能时,要注明对应版本。errors.AsType和new(expr)要求go 1.26或更高版本;strings.CutLast、bytes.CutLast和标准库uuid包要求go 1.27或更高版本。 - 不要把模块已有的写法误判为过时。 如果模块的
go指令版本无法使用替代写法,那么现有惯用写法就不算过时。go 1.24模块可以使用手写的b.N循环和v := expr; &v;指出这些写法的问题,同样属于反方向的误报。 - 机械化兜底。 Go 1.27 的
go test默认运行stdversionvet 检查。它会根据go.mod中的go指令以及文件上的构建标签,报告所引用文件中版本要求高于当前go版本的标准库符号。建议将其作为此规则的强制检查手段。它不检查语言特性或第三方替代方案;这些仍由审查者负责。 - 兼容性陷阱。 规范不得仅为使用便捷辅助函数而提高模块的
go指令版本。库的go指令设定了所有依赖方都必须满足的最低版本:提高它会迫使每个依赖方都使用更新的工具链,才能构建该库。要求规范说明提高版本带来的价值,或者保持原版本并有意使用旧写法。
第 2 步——Go 理念检查
| 关注点 | 检查内容 |
|---|---|
| 简洁性 | 是否存在不必要的层次、只有一种实现的抽象或过度设计? |
| 依赖项 | 是否逐一说明每个拟引入的第三方依赖相较于标准库方案的必要性?(路由、切片/映射辅助工具、多错误处理等) |
| 平台与工具链 | 是否有依赖项、后台服务或手写机制在重复实现编译器、运行时、工具链或平台已有的功能?例如,使用 //go:embed 替代资源处理流水线、使用 os.Root 进行路径隔离、使用构建标签或 tool 指令替代脚本、使用数据库约束替代应用代码中的重复实现。完整的必要性阶梯见 go 技能;应在规范层面应用,因为规范阶段拒绝过度构建的成本最低 |
| 接口 | 接口是否由使用方而非实现方定义?是否足够小(1–3 个方法)?是否仅在确实需要多态的地方使用? |
| 错误处理 | 是否显式返回错误?是否使用 fmt.Errorf("%w", err) 正确包装?是否避免悄然吞掉错误?调用方必须分支处理时,是否为哨兵错误命名? |
| 上下文 | 是否将 context.Context 传递给所有 I/O、HTTP 或长时间运行的调用?是否指定超时? |
| 并发 | Goroutine 是否有明确的所有权和指定的关闭路径?对于输入规模不受限的扇出操作,是否限制了并发数?是否避免了数据竞争? |
| 包设计 | 每个包是否只有一个明确的领域职责?新增包相较于扩展现有包是否有充分理由?是否避免提出 utils//common/ 包? |
| 命名 | 名称是否简短清晰并遵循 Go 约定?是否避免重复命名(pkg.PkgThing)?是否避免使用 Get 前缀? |
| 测试 | 规范是否说明了测试方式?I/O 边界是否使用伪实现/接口,核心逻辑是否使用表驱动用例,CLI 命令是否在内存中执行? |
| YAGNI | 功能和抽象是否仅由明确提出的需求驱动,而非预想未来需求? |
步骤 2b — 新版语言特性检查
在批准引入接口、泛型或函数类型接线的设计前,先运行此检查。如果 Go 1.26/1.27 的语言特性能够简化某项设计,而规格却仍提议采用复杂方案,那么这就是一个 Issue——实现者要么构建变通方案,要么悄悄简化设计,导致实现偏离规格。
| 特性 | 引入版本 | 检查要点 |
|---|---|---|
new(expr) | 1.26 | 是否有辅助函数或临时变量,而 new(expr) 可以直接表达该值 |
| 自引用类型参数约束 | 1.26 | 规格是否声称自引用非法,或为此携带变通辅助函数——这一限制已解除,type Adder[A Adder[A]] interface { Add(A) A } 合法 |
| 泛型方法 | 1.27 | 是否有一种方法繁多的设计,复制了泛型包级辅助函数,而不是在方法上声明类型参数;math/rand/v2 现在便采用后一种做法,在包级 N 之外还提供了 (*Rand).N。请检查规格是否触及以下两项硬性限制:接口的方法不得声明类型参数,且接口方法不能由泛型方法实现——依赖其中任一做法的设计都无法通过编译 |
| 通用函数类型推断 | 1.27 | 是否有显式类型实参,而现在类型推断即可提供——只要将泛型函数赋给匹配的函数类型变量,或转换为该类型,此规则就适用 |
| 结构体字面量字段选择器 | 1.27 | 规格是否坚持要求结构体字面量的键必须是结构体类型的顶层字段名;现在允许使用该结构体类型的任何有效字段选择器 |
步骤 3 — Cobra/Viper CLI 检查(如果不是 CLI,则跳过)
| 关注点 | 检查要点 |
|---|---|
| 命令构建 | 新命令是否通过工厂函数构建,而不是包级 var?(遗留全局变量代码库:参见步骤 1 中关于标志名称的注意事项) |
| 命令注册 | 新子命令是否已注册到父命令?这是否列在规格的文件清单中? |
| RunE 与 Run | 是否使用 RunE 以便传播错误;Run 会悄悄吞掉错误 |
| 持久标志与本地标志 | 跨命令通用的标志是否设为根命令上的持久标志;特定操作的标志是否设在子命令上 |
| Args 验证 | 位置参数数量是否通过 cobra.Args 验证器处理,而不是在 RunE 内处理? |
| Viper 绑定 | 环境变量名称和配置键是否显式绑定?配置结构体中的每个键是否都设置了默认值(AutomaticEnv + Unmarshal 陷阱)?业务逻辑是否接收类型明确的配置结构体,而不是 Viper 本身? |
| 输出 | 命令输出是否通过 cmd.OutOrStdout() 写入,以便进行测试? |
| I/O 路径冲突 | 如果命令接受 --input 和 --output,是否设有防护,避免二者解析为同一路径? |
步骤 4 — 通用规格完整性检查
| 类别 | 检查要点 |
|---|---|
| 完整性 | 是否存在 TODO、占位符、“TBD”、不完整的章节或缺失的错误处理路径 |
| 一致性 | 是否存在内部矛盾、相互冲突的要求,或不同章节对同一类型使用不同名称 |
| 清晰度 | 要求是否含糊到足以导致不同实现者构建出不同内容 |
| 范围 | 是否聚焦到足以制定单一实施计划?是否涵盖多个相互独立的子系统? |
| 数据流 | 是否明确每个函数或步骤的输入和输出 |
| 兼容性 | 如果此变更会改变现有行为、标志、配置键或 API,是否说明了迁移或弃用方案? |
| 安全性 | 用户提供的值在用于 shell 命令、文件路径或外部调用前,是否经过清理? |
校准
仅标记会在实现过程中造成实际问题的事项。 缺少错误处理路径、会导致编译失败的标志命名冲突、无法带来任何作用却增加复杂度的抽象——这些都属于问题。轻微的措辞偏好、格式不一致以及“可以写得更详细”都不属于问题。
存在歧义不代表一定有问题。如果需求不清楚,但规范本身没有其他问题,请将其作为问题向作者提问,而不是阻止实现。
除非存在会导致实现有缺陷或不完整的缺漏,否则请批准。
输出格式
Go 规范审查
状态: 已批准 | 已批准,但有问题 | 发现问题
问题(阻止实现):
- [第 X 节]:[具体问题] — [为什么这会影响实现]
向作者提问(需要答复,但不阻止合理设计):
- [第 Y 节]:[歧义所在] — [实现者可能采取的两种或更多理解]
建议(仅供参考):
- [有助于提高正确性、惯用性或清晰度的建议]