1 分钟

使用 Claude Code 搭建 Go API 脚手架:统一的处理器与服务模式

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

使用 Claude Code 搭建 Go API 脚手架:统一的处理器与服务模式

为什么如果早期不固定模式,Go API 会变得混乱

Go API 往往一开始很干净:几个端点,一两个人,所有东西都在脑子里。然后 API 变大、功能在压力下交付,细微差异悄悄出现。每个差异看似无害,但累积起来会拖慢每一次改动。

一个常见例子:一个 handler 将 JSON 解码到 struct 并返回 400 带有友好信息,另一个返回 422 形状不同,第三个用不同格式记录错误。这些都不会让编译失败,但每次添加新东西时都会产生不断的决策和微小改写。

你会在这些地方感觉到混乱:

  • 不同端点返回不同的错误体,导致客户端需要特殊处理。
  • Service 有时返回原始数据库错误,有时返回“友好”的错误。
  • 命名漂移(CreateUserAddUserRegisterUser)让搜索变得困难。
  • 验证分散到各处,同样的 bug 不断重现。

这里的“脚手架”指的是一个可复用的模板:新工作放在哪、每层职责是什么、响应长什么样。重点不是生成大量代码,而是锁定一致的结构。

像 Claude 这样的工具可以帮助你快速生成端点,但它们只有在你把模式当作规则时才有用。你来定义规则、审查每个 diff 并运行测试。模型负责填充标准部分;它不能重新定义你的架构。

选择一种层级划分:handler、service 和 data access

当每个请求遵循相同路径时,Go API 更容易扩展。在开始生成端点之前,选择一种层级划分并坚持下去。

职责,保持简单

handler 的职责仅限于 HTTP:读取请求、调用 service、写响应。它不应该包含业务规则、SQL 或“只是这个特殊情况”的逻辑。

service 负责用例:业务规则、决策和跨仓库或外部调用的编排。它不应该关心 HTTP 细节,比如状态码、头或错误如何渲染。

数据访问(repository/store)负责持久化细节。它将 service 的意图翻译为 SQL/查询/事务。它不应强制执行业务规则(基本数据完整性除外),也不应塑造 API 响应。

一个实用的分层检查清单:

  • Handler:解析输入、调用 service、将错误映射到 HTTP、写入 JSON
  • Service:应用业务规则、调用 repo、返回领域结果或类型化错误
  • Data access:执行查询、将行映射为 struct、返回存储错误
  • 共享:通用错误类型和响应 helper
  • 不要跳过层(handler 不要直接调用 repo)

验证的一条规则

选定一条规则并不要妥协。

一个简单的做法:

  • Handler 做“形状验证”(必填字段、基本格式)。
  • Service 做“含义验证”(权限、不变量、状态)。

例如: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 映射既容易生成也方便客户端在输入旁显示。

接下来,写下一份状态码映射并严格遵守。每个端点的争论越少越好。如果你要同时使用 400422,就把划分写清楚:

  • bad_json -> 400 Bad Request(格式错误的 JSON)
  • validation_failed -> 422 Unprocessable Content(JSON 格式正确但字段无效)
  • not_found -> 404 Not Found
  • conflict -> 409 Conflict(重复键、版本不匹配)
  • unauthorized -> 401 Unauthorized
  • forbidden -> 403 Forbidden
  • internal -> 500 Internal Server Error

决定哪些信息记录日志,哪些信息返回给客户端。一个好的规则是:客户端得到安全的消息和 request ID;日志包含完整错误和内部上下文(SQL、上游载荷、用户 ID),这些不应该泄露给客户端。

最后,标准化 request_id。如果收到上游(例如 API 网关)传来的 ID,就接受;否则在边缘(中间件)生成一个。把它附到 context、写入日志,并在每个错误响应中返回。

使文件夹布局和命名可预测

要让脚手架保持一致,你的文件夹布局必须无趣且可复现。生成器会跟随它能看到的模式,但当文件散落或命名因功能而异时会出现漂移。

选定一套命名约定并坚持:为每种事物选一个词并保持一致:handler、service、repo、request、response。如果路由是 POST /users,文件和类型以 userscreate 命名(不要有时用 register,有时用 addUser)。

一个与常见层级匹配的简单布局:

internal/
  httpapi/
    handlers/
    users_handler.go
  services/
    users_service.go
  data/
    users_repo.go
  apitypes/
    users_types.go

决定共享类型放在哪里,因为项目往往在这一步变混乱。一个有用的规则:

  • API 请求/响应类型放在 internal/apitypes(匹配 JSON 和验证需求)。
  • 领域类型更靠近 service 层(匹配业务规则)。

如果一个类型有 JSON tag 并为客户端设计,就把它当作 API 类型。

保持 handler 的依赖最小并明确这条规则:

  • Handlers 只导入:routing/http、context、apitypes 和 services
  • Services 导入:领域类型和数据访问
  • Data access 导入:数据库驱动和查询 helper
  • Handler 不直接导入数据库包

在仓库根目录写一份简短的模式文档(纯 Markdown 即可)。包括文件树、命名规则和一个小示例流程(handler -> service -> repo,以及各部分属于哪个文件)。这是你粘贴到生成器里的精确参考,使新端点每次都匹配结构。

创建一个参考端点来设定模式

Keep handlers thin by default
Add DTOs, typed errors, and helpers in one place so each endpoint matches.

在生成十个端点之前,先创建一个你信任的端点。这是金标准:你可以指着这个文件说,“新代码必须像这样”。你可以从零写它,也可以重构现有端点直到它符合标准。

保持 handler 精简。一个有帮助的做法是:在 handler 和 service 之间放一个接口,让 handler 依赖契约而不是具体实现。

在参考端点中只在生成代码可能出错的地方添加少量注释。解释决策(为何 400 而不是 422、为何 create 返回 201、为何对内部错误使用通用消息)。跳过那些只是重复代码的注释。

参考端点工作后,抽取 helper,这样每个新端点就更不容易出现漂移。最可复用的 helper 通常是:

  • 绑定 JSON 并处理格式错误的 body
  • 验证输入并返回字段级错误
  • 一致地写 JSON 响应
  • 将领域错误映射到 HTTP 状态码

下面展示“精简 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)
}

用几项测试(即使只是用于错误映射的表驱动测试)把它钉死。生成器最好有一个干净的目标去模仿。

逐步:让 Claude 生成与模式匹配的新端点

一致性始于你粘贴的内容与禁止项。对于新端点,提供两样东西:

  1. 你的参考端点(“完美”示例)
  2. 简短的模式说明,标明包、函数和必须遵循的 helper

1) 先提供上下文(参考 + 规则)

包含 handler、service 方法、请求/响应类型以及端点使用的任何共享 helper。然后用明文说明契约:

  • 路由 + 方法(示例:POST /v1/widgets
  • 请求 JSON 字段(必填 vs 可选)
  • 响应 JSON 形状
  • 错误情况和状态码
  • 你期望生成的文件(并且仅限这些文件)

明确声明必须匹配的内容:命名、包路径和 helper 函数名称(WriteJSONBindJSONWriteError、你的 validator)。

2) 要求以你想要的精确形式输出

严格的提示能防止“有用的”重构。例如:

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 端点

Plan the layers first
Map handlers, services, and repos before generating code to reduce drift.

假设你要添加一个 “Create project” 端点。目标很简单:接受 name,强制一些规则,存储并返回新的 ID。难点是保持 handler–service–repo 的同一分工和已用的错误 JSON。

一致的流程如下:

  • Handler:绑定 JSON,做基本字段验证,调用 service
  • Service:应用业务规则(如唯一性),调用 repo,返回领域结果
  • Repo:写入 Postgres,返回生成的 ID

下面是 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 或错误包,都会变成另一种样式需要匹配。

防止大多数破坏的护栏:

  • 要求新端点重用现有错误类型和 helpers。
  • 保持 handler 无业务规则和数据库访问。
  • 强制统一的请求/响应 struct 命名约定。
  • 将内部错误映射到公共错误码,绝不直接返回原始错误。
  • 新增依赖必须有明确且书面的理由。

如果你无法通过 diff 比较两个端点并看到相同的结构(导入、流程、错误处理),就在合并前收紧提示并重新生成。

在合并生成的端点前的快速清单

Ship and test faster
Host your API after scaffolding so you can test real client behavior early.

在合并任何生成代码前,先检查结构。如果结构正确,逻辑错误更容易发现。

结构检查:

  • Handler 流程一致:绑定输入、验证、调用 service、将领域错误映射为 HTTP、写响应。
  • Service 代码仅包含业务规则:没有直接的 HTTP 或 JSON 工作。
  • 成功响应符合你的风格(要么每处包一层共享信封,要么直接返回 JSON)。
  • 错误响应统一:相同的 JSON 字段、相同的 code、相同的 request_id 行为。
  • 命名和放置平凡一致:文件名、函数名和路由名与现有端点匹配,并且都经过 gofmt 格式化。

行为检查:

  • 运行测试并为新 handler/service 的一个分支至少添加一个小测试。
  • 确认验证失败返回与类似端点相同的代码和状态码。
  • 触发一个已知的 service 错误(比如“not found”或“conflict”)并确认 HTTP 状态与 JSON 形状。
  • 扫描复制粘贴留下的问题:错误的路由路径、错误的日志消息、不匹配的 DTO 名称。
  • 本地构建并运行服务器一次以确保 wiring 和导入正确。

下一步:把模式标准化,然后安全地扩展生成

把你的模式当作共享契约,而不是个人偏好。把“我们如何构建端点”的文档放在代码旁边,维护一个显示端到端方法的参考端点。

小批量地扩展生成:生成 2 到 3 个端点,覆盖不同边界(一个简单的读取、一个带验证的创建、一个有 not-found 情况的更新)。然后停下来复盘。如果审查持续发现相同的风格漂移,就先更新基线文档和参考端点,再继续生成更多。

一个可重复的循环:

  • 写下基线:文件名、函数名、请求/响应结构、错误码,以及验证发生的位置。
  • 保持一个“金牌”端点,模式改变时先更新它。
  • 批量生成一些端点,审查一致性,然后调整提示和模式文档。
  • 分块重构旧端点,并保留回滚路径以防行为改变。
  • 跟踪一个指标一周:添加端点所需时间、合并后 bug 率或审查时间。

如果你想要更紧的构建—审查循环,像 Koder.ai 这样的 vibe-coding 平台可以帮助你在聊天驱动的工作流中快速搭建并迭代,然后在结构符合标准时导出源码。工具不如规则重要:你的基线必须主导。

常见问题

What’s the fastest way to stop a Go API from getting inconsistent?

锁定一个可重复的模板:统一的层级划分(handler → service → data access)、统一的错误信封和状态码映射。然后把每个新端点都要求按这个“参考端点”来实现。

What should a handler do (and not do)?

保持处理器只做 HTTP 相关工作:

  • 绑定/解析请求
  • 做基本的“形状校验”(必填字段、简单格式)
  • 调用 service
  • 将有类型的错误映射到 HTTP 状态码
  • 使用共享 helper 写 JSON

如果在 handler 里看到 SQL、权限检查或业务分支,把它们移到 service。

What belongs in the service layer?

把业务规则和决策放在 service:

  • 权限与访问规则
  • 不变量(状态转换、唯一性规则、“允许”检查)
  • 在多个仓库或外部调用间编排

service 应返回领域结果和类型化错误——不要返回 HTTP 状态或做 JSON 成形。

What belongs in the data access/repository layer?

把持久化相关的关注点隔离开:

  • SQL/查询和事务
  • 将行映射为 struct
  • 返回存储层错误(由 service 解读)

避免在 repo 中编码 API 响应格式或强制业务规则(除基本数据完整性外)。

Where should validation live?

一个简单的默认做法:

  • Handlers 验证请求的“形状”(缺失字段、基本格式)
  • Services 验证“含义”(权限、不变量、状态)

例如:handler 检查 email 是否存在且看起来像邮箱;service 检查该邮箱是否被允许且未被占用。

What should a standard API error response look like?

在所有地方使用同一个错误信封,并保持稳定。一个实用的结构是:

  • code 用于机器(稳定)
  • message 给人类(简短且安全)
  • details 用于结构化附加信息(比如字段错误)
  • request_id 用于追踪

这能避免客户端出现特殊情况处理,并让生成的端点可预测。

How do I choose between 400 vs 422 vs 409 for errors?

把状态码映射写下来并始终遵守。常见划分:

  • 400 用于格式错误的 JSON(bad_json
  • 422 用于验证失败(validation_failed
  • 404 资源不存在(not_found
  • 409 冲突(重复/版本不匹配)
  • 500 意外失败

关键是保持一致:不要针对每个端点单独争论。

Should I ever return raw database errors to clients?

返回安全且一致的公共错误,并在日志中记录真实原因。

  • 响应:稳定的 code、简短的 message,外加 request_id
  • 日志:完整错误详情(SQL 错误、上游载荷、用户 ID)

这可以防止泄露内部信息,并避免端点之间出现随机的错误消息差异。

What is a reference endpoint, and why do I need one?

创建一个“金牌”参考端点,并要求新端点与之保持一致:

  • 相同流程(bind → validate → service → error map → JSON)
  • 相同的 helper(BindJSONWriteJSONWriteError 等)
  • 相同的文件组织和命名

然后为它添加几个小测试(例如用于错误映射的表驱动测试),把这个模式钉死。

How do I prompt Claude to generate new endpoints without breaking my structure?

给模型严格的上下文和约束:

  • 粘贴参考端点和模式规则
  • 指定路由、请求/响应 JSON 示例和错误情况
  • 明确列出它可以输出的文件(并仅限这些文件)
  • 明确说明不得修改的内容(router、错误格式、helpers)

生成后,拒绝那些“改进”架构但违反基线的 diff。

Related posts