使用 Claude Code 为 Go API 定义统一的 handler–service–error 模式,然后生成保持一致性的新端点。

Go API 往往一开始很干净:几个端点,一两个人,所有东西都在脑子里。然后 API 变大、功能在压力下交付,细微差异悄悄出现。每个差异看似无害,但累积起来会拖慢每一次改动。
一个常见例子:一个 handler 将 JSON 解码到 struct 并返回 400 带有友好信息,另一个返回 422 形状不同,第三个用不同格式记录错误。这些都不会让编译失败,但每次添加新东西时都会产生不断的决策和微小改写。
你会在这些地方感觉到混乱:
CreateUser、AddUser、RegisterUser)让搜索变得困难。这里的“脚手架”指的是一个可复用的模板:新工作放在哪、每层职责是什么、响应长什么样。重点不是生成大量代码,而是锁定一致的结构。
像 Claude 这样的工具可以帮助你快速生成端点,但它们只有在你把模式当作规则时才有用。你来定义规则、审查每个 diff 并运行测试。模型负责填充标准部分;它不能重新定义你的架构。
当每个请求遵循相同路径时,Go API 更容易扩展。在开始生成端点之前,选择一种层级划分并坚持下去。
handler 的职责仅限于 HTTP:读取请求、调用 service、写响应。它不应该包含业务规则、SQL 或“只是这个特殊情况”的逻辑。
service 负责用例:业务规则、决策和跨仓库或外部调用的编排。它不应该关心 HTTP 细节,比如状态码、头或错误如何渲染。
数据访问(repository/store)负责持久化细节。它将 service 的意图翻译为 SQL/查询/事务。它不应强制执行业务规则(基本数据完整性除外),也不应塑造 API 响应。
一个实用的分层检查清单:
选定一条规则并不要妥协。
一个简单的做法:
例如:handler 检查 email 是否存在并且看起来像邮箱;service 检查该邮箱是否被允许且未被占用。
尽早决定 services 返回领域类型还是 DTO。
一个干净的默认是:handler 使用请求/响应 DTO,service 使用领域类型,handler 负责把领域类型映射为响应。这让 service 即使在 HTTP 合约变化时也保持稳定。
如果映射看起来繁琐,也要保持一致:让 service 返回领域类型加上有类型的错误,把 JSON 组织留在 handler。
如果你希望生成的端点看起来像同一个人写的,就要尽早锁定错误响应。生成效果最好当输出格式不可协商时:一个 JSON 形状、一套状态码映射、一条关于可暴露内容的规则。
从一个统一的错误信封开始,所有端点在失败时都返回这个形状。保持它小且可预测:
{
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": {
"fields": {
"email": "must be a valid email address",
"age": "must be greater than 0"
}
},
"request_id": "req_01HR..."
}
使用 code 给机器(稳定且可预测),message 给人类(简短且安全)。把可选的结构化数据放到 details。对于验证,简单的 details.fields 映射既容易生成也方便客户端在输入旁显示。
接下来,写下一份状态码映射并严格遵守。每个端点的争论越少越好。如果你要同时使用 400 和 422,就把划分写清楚:
bad_json -> 400 Bad Request(格式错误的 JSON)validation_failed -> 422 Unprocessable Content(JSON 格式正确但字段无效)not_found -> 404 Not Foundconflict -> 409 Conflict(重复键、版本不匹配)unauthorized -> 401 Unauthorizedforbidden -> 403 Forbiddeninternal -> 500 Internal Server Error决定哪些信息记录日志,哪些信息返回给客户端。一个好的规则是:客户端得到安全的消息和 request ID;日志包含完整错误和内部上下文(SQL、上游载荷、用户 ID),这些不应该泄露给客户端。
最后,标准化 request_id。如果收到上游(例如 API 网关)传来的 ID,就接受;否则在边缘(中间件)生成一个。把它附到 context、写入日志,并在每个错误响应中返回。
要让脚手架保持一致,你的文件夹布局必须无趣且可复现。生成器会跟随它能看到的模式,但当文件散落或命名因功能而异时会出现漂移。
选定一套命名约定并坚持:为每种事物选一个词并保持一致:handler、service、repo、request、response。如果路由是 POST /users,文件和类型以 users 和 create 命名(不要有时用 register,有时用 addUser)。
一个与常见层级匹配的简单布局:
internal/
httpapi/
handlers/
users_handler.go
services/
users_service.go
data/
users_repo.go
apitypes/
users_types.go
决定共享类型放在哪里,因为项目往往在这一步变混乱。一个有用的规则:
internal/apitypes(匹配 JSON 和验证需求)。如果一个类型有 JSON tag 并为客户端设计,就把它当作 API 类型。
保持 handler 的依赖最小并明确这条规则:
在仓库根目录写一份简短的模式文档(纯 Markdown 即可)。包括文件树、命名规则和一个小示例流程(handler -> service -> repo,以及各部分属于哪个文件)。这是你粘贴到生成器里的精确参考,使新端点每次都匹配结构。
在生成十个端点之前,先创建一个你信任的端点。这是金标准:你可以指着这个文件说,“新代码必须像这样”。你可以从零写它,也可以重构现有端点直到它符合标准。
保持 handler 精简。一个有帮助的做法是:在 handler 和 service 之间放一个接口,让 handler 依赖契约而不是具体实现。
在参考端点中只在生成代码可能出错的地方添加少量注释。解释决策(为何 400 而不是 422、为何 create 返回 201、为何对内部错误使用通用消息)。跳过那些只是重复代码的注释。
参考端点工作后,抽取 helper,这样每个新端点就更不容易出现漂移。最可复用的 helper 通常是:
下面展示“精简 handler + 接口”在实践中的样子(代码块保持不变):
type UserService interface {
CreateUser(ctx context.Context, in CreateUserInput) (User, error)
}
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
var in CreateUserRequest
if err := BindJSON(r, &in); err != nil {
WriteError(w, ErrBadJSON) // 400: malformed JSON
return
}
if err := Validate(in); err != nil {
WriteError(w, err) // 422: validation details
return
}
user, err := h.svc.CreateUser(r.Context(), in.ToInput())
if err != nil {
WriteError(w, err)
return
}
WriteJSON(w, http.StatusCreated, user)
}
用几项测试(即使只是用于错误映射的表驱动测试)把它钉死。生成器最好有一个干净的目标去模仿。
一致性始于你粘贴的内容与禁止项。对于新端点,提供两样东西:
包含 handler、service 方法、请求/响应类型以及端点使用的任何共享 helper。然后用明文说明契约:
POST /v1/widgets)明确声明必须匹配的内容:命名、包路径和 helper 函数名称(WriteJSON、BindJSON、WriteError、你的 validator)。
严格的提示能防止“有用的”重构。例如:
Using the reference endpoint below and the pattern notes, generate a new endpoint.
Contract:
- Route: POST /v1/widgets
- Request: {"name": string, "color": string}
- Response: {"id": string, "name": string, "color": string, "createdAt": string}
- Errors: invalid JSON -> 400; validation -> 422; duplicate name -> 409; unexpected -> 500
Output ONLY these files:
1) internal/http/handlers/widgets_create.go
2) internal/service/widgets.go (add method only)
3) internal/types/widgets.go (add types only)
Do not change: router setup, existing error format, existing helpers, or unrelated files.
Must use: package paths and helper functions exactly as in the reference.
如果你要测试,请明确请求测试并命名测试文件。否则模型可能会跳过测试或虚构测试设置。
生成后做一个快速 diff 检查。如果它修改了共享 helper、路由注册或标准错误响应,就拒绝输出并更严格地重申“不可修改”规则。
输出的一致性取决于你提供的输入。避免“几乎正确”代码的最快方式是每次重用一个提示模板,并从仓库中粘贴一小段上下文快照。
粘贴并填充占位符:
You are editing an existing Go HTTP API.
CONTEXT
- Folder tree (only the relevant parts):
<paste a small tree: internal/http, internal/service, internal/repo, etc>
- Key types and patterns:
- Handler signature style: <example>
- Service interface style: <example>
- Request/response DTOs live in: <package>
- Standard error response JSON:
{
"error": {
"code": "invalid_argument",
"message": "...",
"details": {"field": "reason"}
}
}
- Status code map:
invalid_json -> 400
invalid_argument -> 422
not_found -> 404
conflict -> 409
internal -> 500
TASK
Add a new endpoint: <METHOD> <PATH>
- Handler name: <Name>
- Service method: <Name>
- Request JSON example:
{"name":"Acme"}
- Success response JSON example:
{"id":"123","name":"Acme"}
CONSTRAINTS
- No new dependencies.
- Keep functions small and single-purpose.
- Match existing naming, folder layout, and error style exactly.
- Do not refactor unrelated files.
ACCEPTANCE CHECKS
- Code builds.
- Existing tests pass (add tests only if the repo already uses them for handlers/services).
- Run gofmt on changed files.
FINAL INSTRUCTION
Before writing code, list any assumptions you must make. If an assumption is risky, ask a short question instead.
这会强制三件事:上下文块(已有内容)、约束块(不能做的事)和具体的 JSON 示例(避免形状漂移)。最后的指令是安全检查:如果模型不确定,它应该在写代码前先问。
假设你要添加一个 “Create project” 端点。目标很简单:接受 name,强制一些规则,存储并返回新的 ID。难点是保持 handler–service–repo 的同一分工和已用的错误 JSON。
一致的流程如下:
下面是 handler 接受的请求示例:
{ "name": "Roadmap", "owner_id": "u_123" }
成功时返回 201 Created。ID 应该来自一个固定来源。例如让 Postgres 生成 ID,repo 返回它:
{ "id": "p_456", "name": "Roadmap", "owner_id": "u_123", "created_at": "2026-01-09T12:34:56Z" }
两个现实的失败路径:
如果验证失败(缺失或过短的 name),使用标准形状返回字段级错误并使用约定的状态码:
{ "error": { "code": "VALIDATION_ERROR", "message": "Invalid request", "details": { "name": "must be at least 3 characters" } } }
如果名称必须在 owner 范围内唯一且 service 发现已有项目,返回 409 Conflict:
{ "error": { "code": "PROJECT_NAME_TAKEN", "message": "Project name already exists", "details": { "name": "Roadmap" } } }
一条保持模式干净的决策:handler 检查“请求是否形状正确?”,service 负责“这是否被允许?”。这种分离让生成的端点更可预测。
让生成器随意发挥是失去一致性的最快方式。
一种常见漂移是新错误形状。一个端点返回 {error: "..."},另一个返回 {message: "..."},第三个又嵌套了对象。通过把错误信封和状态码映射放在统一位置并要求新端点按导入路径和函数名重用它们来修正这个问题。如果生成器提议新字段,把它当作 API 变更请求而不是便利性改动。
另一个漂移是 handler 膨胀。它开始很小:验证,然后检查权限,然后查询 DB,然后根据业务规则分支。很快每个 handler 看起来都不一样。坚持一条规则:handler 把 HTTP 翻译成类型化的输入输出;service 负责决策;data access 负责查询。
命名不一致也会累计成本。如果一个端点使用 CreateUserRequest 而另一个使用 NewUserPayload,你会浪费时间寻找类型并写胶水代码。选定命名方案并拒绝新名字,除非有充分理由。
绝不要把原始数据库错误返回给客户端。除了泄露细节外,这会导致不一致的消息和状态码。封装内部错误、在日志记录真实原因,并返回稳定的公共错误码。
避免为了“方便”加入新库。每增加一个验证器、路由 helper 或错误包,都会变成另一种样式需要匹配。
防止大多数破坏的护栏:
如果你无法通过 diff 比较两个端点并看到相同的结构(导入、流程、错误处理),就在合并前收紧提示并重新生成。
在合并任何生成代码前,先检查结构。如果结构正确,逻辑错误更容易发现。
结构检查:
request_id 行为。行为检查:
把你的模式当作共享契约,而不是个人偏好。把“我们如何构建端点”的文档放在代码旁边,维护一个显示端到端方法的参考端点。
小批量地扩展生成:生成 2 到 3 个端点,覆盖不同边界(一个简单的读取、一个带验证的创建、一个有 not-found 情况的更新)。然后停下来复盘。如果审查持续发现相同的风格漂移,就先更新基线文档和参考端点,再继续生成更多。
一个可重复的循环:
如果你想要更紧的构建—审查循环,像 Koder.ai 这样的 vibe-coding 平台可以帮助你在聊天驱动的工作流中快速搭建并迭代,然后在结构符合标准时导出源码。工具不如规则重要:你的基线必须主导。
锁定一个可重复的模板:统一的层级划分(handler → service → data access)、统一的错误信封和状态码映射。然后把每个新端点都要求按这个“参考端点”来实现。
保持处理器只做 HTTP 相关工作:
如果在 handler 里看到 SQL、权限检查或业务分支,把它们移到 service。
把业务规则和决策放在 service:
service 应返回领域结果和类型化错误——不要返回 HTTP 状态或做 JSON 成形。
把持久化相关的关注点隔离开:
避免在 repo 中编码 API 响应格式或强制业务规则(除基本数据完整性外)。
一个简单的默认做法:
例如:handler 检查 email 是否存在且看起来像邮箱;service 检查该邮箱是否被允许且未被占用。
在所有地方使用同一个错误信封,并保持稳定。一个实用的结构是:
code 用于机器(稳定)message 给人类(简短且安全)details 用于结构化附加信息(比如字段错误)request_id 用于追踪这能避免客户端出现特殊情况处理,并让生成的端点可预测。
把状态码映射写下来并始终遵守。常见划分:
400 用于格式错误的 JSON()返回安全且一致的公共错误,并在日志中记录真实原因。
code、简短的 message,外加 request_id这可以防止泄露内部信息,并避免端点之间出现随机的错误消息差异。
创建一个“金牌”参考端点,并要求新端点与之保持一致:
BindJSON、WriteJSON、WriteError 等)然后为它添加几个小测试(例如用于错误映射的表驱动测试),把这个模式钉死。
给模型严格的上下文和约束:
生成后,拒绝那些“改进”架构但违反基线的 diff。
bad_json422 用于验证失败(validation_failed)404 资源不存在(not_found)409 冲突(重复/版本不匹配)500 意外失败关键是保持一致:不要针对每个端点单独争论。