Nhost Go 设计规则详解:三问审查框架与 Monorepo 级 Go 工程规范(.claude/docs/go-design-rules.md)
2026/9/16 19:38:10 网站建设 项目流程

Nhost Go 设计规则详解:三问审查框架与 Monorepo 级 Go 工程规范(.claude/docs/go-design-rules.md)

【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost

Nhost 仓库将 Go 代码的设计规范独立沉淀在 .claude/docs/go-design-rules.md 中,作为覆盖services/*cli/internal/lib/tools/及未来所有 Go 代码的权威设计规则。本文完整拆解这套规则的核心脉络——"三问"审查框架、Blocking/Warning/Suggestion 三级标准、强制的变更后检查命令,以及模块级工程约束,并结合仓库中真实的 lint 配置、mockgen 指令与参数化 SQL 管道实现,说明每条规则在 Nhost 这个 Go + TypeScript 混合 monorepo 中的落地方式与验证路径。

一、这套文档在 Nhost 仓库中的定位

Nhost 是一个混合 monorepo:Go 编写的后端服务(services/)、CLI(cli/)、共享库(internal/lib/)、工具(tools/),加上 TypeScript 编写的 dashboard、SDK 与文档站。仓库根目录的 CLAUDE.md 明确指出:权威设计规则位于.claude/docs/,写 Go 代码或评审 Go 变更前必须加载 go-design-rules.md,它与 javascript-design-rules.md 构成语言级的成对规范,而各子项目的CLAUDE.md(如 services/constellation/CLAUDE.md)再在其上层叠加项目特有不变量。

这份文档的设计动机在开篇就点明:它刻意比golangci-lint能检出的内容更严格,因为它的目标对象是 linter 看不见的"设计与架构"问题——符号是否放对了包、是否破坏了包的既有不变量、改动行本身是否合规。仓库的 lint 配置同样与它互相咬合:.golangci.yaml 中可以看到注释明确写道 "we standardize onexhaustructper.claude/docs/go-design-rules.md",即设计文档是配置取舍的依据,而非反之。

二、模块级约束(Module-wide Constraints)

文档首先固定了整个 monorepo 的 Go 工程基线:

  • Go 版本固定为 1.27.0。这与 go.mod 第一、三行的事实完全一致:module github.com/nhost/nhost+go 1.27.0
  • 根目录单go.mod:模块路径是github.com/nhost/nhost禁止为任何子项目新增独立的go.modvendor/。这意味着services/authservices/constellation等服务并不各自成模块,而是共享同一份依赖树。
  • 依赖变更后,必须从仓库根执行go mod vendor影响代码生成的变更后,执行go generate ./...
  • Lint 配置统一放在根目录 .golangci.yaml,该配置启用default: all全量 linter,并配有针对测试文件(豁免 funlen/ireturn/gosec 等)与特定生成文件(如schema.resolvers.go)的精细排除规则。

文档还划定了审查边界:跳过vendor/目录和生成文件*_gen.go*.gen.gogenerated.goschema.resolvers.go);不要重复标记严格跑一遍golangci-lint --fix ./...就能自动修复的机械问题(格式化、未使用变量、基础结构体穷尽性)——这些是作者合并前自己的责任。这个边界很重要:它把"设计评审"与"格式化工具能做的事"切分开,避免评审精力被工具可自动修复的噪音稀释。

三、三问框架:每个变更都必须回答三个问题

这是整套规则的方法论核心。文档要求评审任何 Go 符号变更时,每条 finding 必须归属于以下三问之一:

  1. Placement(归属,Q1)——这个变更在正确的包里吗?新增/修改的代码是否契合该包唯一的关注点,还是应该放进兄弟包或新建子包?
  2. Package invariants(包不变量,Q2)——这个变更是否让该包违反了它原本满足的设计规则?文档列举的典型反例包括:新增未测试的导出符号、没有接口的新技术栈依赖、带//nolint:exhaustruct的内部结构体、未受控的数据库特性、过期的 golden file。
  3. Local correctness(局部正确性,Q3)——改动的这几行本身是否合规?涵盖错误包装、裸return errexport_test再导出、SQL 参数化管道、安全、逻辑正确性、表驱动测试、godoc 等。

文档强调:写代码时同样用这三个视角反向自检——把符号放进正确的包、不破坏包的不变量、让改动行第一次就通过局部正确性规则。最后一句是全篇的方法论点睛:"读周围包,而不只是 diff hunk。diff 告诉你什么变了,包上下文才告诉你这个变更对不对"。

四、🔴 Blocking 规则:合并前必须修复

Blocking 级问题分四个组。

4.1 安全与正确性

  • SQL 注入:任何用户提供的值进入 SQL,必须走参数化管道(占位符 + 参数切片),严禁字符串拼接。文档要求构建 SQL 的连接器必须让值穿过dialect.Placeholder(paramIndex)(或项目中等价的VariableTracker风格 API),使数据库收到的是$1/?而非原始值。这一要求在仓库中有直接实现印证:services/constellation/connector/sql/graphql/queries/values/values.go 文件头注释即声明其职责是 "VariableTracker, ensuring user-supplied data never reaches SQL by string [concatenation]"。Constellation 是同时支持 PostgreSQL 与 SQLite 的 GraphQL 引擎,跨方言的值绑定正是这类规则最真实的战场。
  • 其他安全问题:命令注入、硬编码密钥、凭据写入日志、未消毒的输入跨越信任边界。
  • Bug / 逻辑错误:off-by-one、nil 解引用、错误分支、并发破坏、错误的状态变更。文档还特指一类隐蔽 bug:绕过包的公开 builder 直接修改共享的原子指针状态

4.2 归属问题(Q1)

  • 内聚性:新/改符号的用途与包的关注点不清晰匹配时,应建议移入现有兄弟包或抽取子包。
  • 硬编码方言/后端语法:任何在不同后端之间存在差异的新代码(例如 PostgreSQL 与 SQLite 的 SQL 语法差异)必须穿过项目抽象接口,绝不允许在调用点硬编码

4.3 包不变量(Q2)

  • 无接口的外部系统边界:新增对数据库、HTTP/REST API、消息队列、文件系统、时钟,或"复杂到只需用到一两个方法的具象类型"的依赖,必须藏在消费方包内定义的接口后面。触发判据是可测试性:如果 fake/mock 能实质简化测试,该依赖就必须是接口。文档特意强调这不是"任何外部模块类型都要接口化"的教条——纯的、易构造的值类型可以直接使用。
  • 缺少 mockgen 指令:每个新的边界接口必须在紧邻上方标注//go:generate mockgen -package mock -destination mock/<name>.go . <Interface>,mock 生成到mock/子目录,禁止手写。仓库中可验证这一惯例已普遍执行,例如 services/constellation/connector/connector.go 第 27 行的//go:generate mockgen -package mock -destination mock/connector.go . Connector,以及同目录下生成的 mock/connector.go。
  • 未测试的新导出:每个新导出函数/方法/类型都必须有测试,且必须放在package foo_test(黑盒)中;用make coverage PACKAGE=<path>确认。该目标在 services/constellation/Makefile 第 39 行真实存在(coverage: ## Run test coverage for PACKAGE (default: ./...))。
  • exhaustructnolint 滥用:对本仓库内定义的类型永不允许//nolint:exhaustruct。仅有的两个例外:不拥有定义权的外部类型、以及错误路径上与错误一起返回的零值结构体。其余场景一律标记。

4.4 局部正确性(Q3)

  • 错误包装:来自其他包或外部依赖的错误必须带上调用点上下文:return fmt.Errorf("loading config: %w", err)。任何做 I/O 或跨包调用的函数中裸return err,以及任何用_吞掉或在if中忽略的错误,都要标记。
  • export_test.go/XxxForTest再导出:禁止。任何"仅为让黑盒_test包够到包私有状态"而导出的标识符(注入私有字段的构造器、访问器、哨兵别名)都要标记;正确做法是白盒foo_internal_test.go+ 内联 stub 类型。唯一例外:为mockgen导出的边界接口属于正当的公共契约。
  • 忽略错误:返回值是 error 时永不写_ = someFunc();永不出现空的if err != nil {}块把错误丢弃。

五、🟡 Warning 规则:下个版本前应修复

Warning 级共 10 条,覆盖"接口化之外"的架构与质量细节:

  • 子包抽取:当代码新增 2–3 个相关导出符号并形成清晰子关注点时,建议抽取子包。
  • 默认非导出:包外无消费者(测试不算)的新导出符号应当 unexported,尤其要盯住导出结构体字段。
  • 内部测试放置:复杂的新非导出逻辑应在foo_internal_test.gopackage foo)中白盒测试;同一文件不得混用package foopackage foo_test;不得用白盒方式测公共符号。另一条硬性约束:白盒测试文件不能 import 本包的mock/子目录(会造成 import cycle),应直接在测试文件里用内联 stub 类型实现接口。
  • 构造器卫生:含必填非零字段的新导出结构体,必须有New*构造器,而不是依赖字面量初始化。
  • 表驱动测试:针对多输入/输出用例的新测试必须用tests := []struct{...}{...}; for _, tt := range tests { t.Run(...) }模式;出现TestFoo1/TestFoo2复制粘贴要标记。
  • Mock 测试与集成测试双轨:mock 单测应覆盖全部逻辑分支;集成测试必须验证 mock 的行为与真实依赖一致。
  • 冗余注释:仅仅复述代码的注释、或依赖读者没有的上下文("needed for X" 却不说明 X 是什么)都要清理。
  • 包级 godoc:新包必须在package声明上写 godoc 注释,说明该包的职责域。
  • 无理由的nolint:只有当"修复的复杂度不划算"时才可接受,且必须附理由注释;裸//nolint要标记。
  • 面向客户变更缺文档:新的公共/客户可见函数或 API 应有文档。

六、🔵 Suggestion 级:可选改进

  • 导出的 godoc:每个导出符号都应有以其名称开头的小写注释。
  • 风格/命名/小重构:与周围代码保持一致即可。

七、强制的变更后检查(Mandatory Post-Change Checks)

这是文档中最具操作性的部分:每次修改 Go 源文件后、报告工作完成前,必须从仓库根按顺序执行:

golines -w --base-formatter=gofumpt . golangci-lint run --fix ./...

两条命令都作用于整个项目而非仅触碰过的文件——目的是捕获连带后果(import 重排、结构体字段穷尽性、死代码)。若命令修改了文件,须把它们重新暂存进同一提交;任何残留的golangci-lintfinding 都视为阻断项:要么修复,要么用带注释的定向//nolint:<linter>给出理由。结合根 .golangci.yaml 可看到这套流程的现实配置细节:funlen上限 65 行、cyclop复杂度上限 15、generated: lax对生成文件放宽检查,与文档"跳过生成文件"的边界相互呼应。

八、项目特有不变量:分层加载机制

文档最后一节把粒度再切细一层:各子项目的 Go 不变量(例如 Constellation 的Dialect/Capabilities/controllerState规则、golden file 再生协议)存放在各自的CLAUDE.md中(如 services/constellation/CLAUDE.md、services/constellation/CLAUDE.md 同层),由 Agent 的启动协议自动加载,叠加在本文上述规则之上应用。这形成三层结构:.claude/docs/的语言级权威规则 → 根CLAUDE.md的结构索引 → 各子项目CLAUDE.md的项目级不变量

九、如何验证与落地这套规则

  • 查看规则全文:.claude/docs/go-design-rules.md;
  • 查看它与 lint 配置的一致性:.golangci.yaml 中exhaustruct取舍注释直接引用了该文档;
  • 查看 mockgen 指令与生成产物的一对一实例:services/constellation/connector/connector.go → services/constellation/connector/mock/connector.go;
  • 查看 SQL 参数化管道实现:services/constellation/connector/sql/graphql/queries/values/values.go;
  • make coverage PACKAGE=<path>(见 services/constellation/Makefile)验证"新导出必须有测试"这条 Blocking 规则。

这套规则的价值在于:它把通常散落在资深工程师脑中的评审判断,显式化为"三问 + 三级"的可执行清单,并用仓库内真实存在的工具链(golines、golangci-lint、mockgen、make coverage)为每一条规则提供了可验证的落点。对于在任何多服务 Go monorepo 中承担设计评审职责的开发者,其结构——先定归属、再查不变量、最后抠局部正确性——具有直接可迁移的参考价值。

【免费下载链接】nhostThe Open Source Firebase Alternative with GraphQL.项目地址: https://gitcode.com/GitHub_Trending/nh/nhost

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询