Contract Primer
Contract Primer
Section titled “Contract Primer”当你已经通过 稳定根 确认改动仍属于默认服务路径,而且问题进一步收窄到服务如何塑造其 HTTP 表面时,就打开这一页:写回什么内容、如何表示错误、在 request context 里携带什么信息。
contract 拥有结构化错误模型、规范的响应助手,以及请求级别的 context 访问器。它是允许停留在传输边界而不引入 app construction、routing 或持久化的模块唯一可用的导入。
func Greet(w http.ResponseWriter, r *http.Request) { rc := contract.RequestContextFromContext(r.Context()) name := rc.Params["name"] _ = contract.WriteResponse(w, r, http.StatusOK, map[string]string{"message": "hello, " + name}, nil)}
func Fail(w http.ResponseWriter, r *http.Request) { _ = contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeNotFound). Message("user not found"). Build())}什么时候从这里开始
Section titled “什么时候从这里开始”- 你正在编写或修改
WriteError或WriteResponse的输出形状 - 你正在添加或修改 error category 或 error type
- 你正在读写请求级别的元数据,例如 request ID、route params 或 trace headers
什么时候不该从这里开始
Section titled “什么时候不该从这里开始”- 改动关心的是 route matching 或 URL parameter 提取策略 — 那属于
router - 改动引入了 session lifecycle、token 签发或 auth identity — 那属于
security - 工作关于 middleware 如何观察或转换请求 — 从
middleware开始 - 改动添加了 protocol-gateway 行为、multipart upload 便捷函数或特定功能的响应信封
当前仓库里先读哪些文件
Section titled “当前仓库里先读哪些文件”contract/module.yamlcontract/response.gocontract/errors.gocontract/context_core.goreference/standard-service/internal/handler/health.go
更具体的归属例子
Section titled “更具体的归属例子”这些工作适合留在 contract | 一旦变成这些问题就应移出 |
|---|---|
WriteError / WriteResponse — 唯一规范的写入路径 | 带有各功能构造函数的特定功能响应信封 |
ErrorCategory、ErrorType 常量与 APIError 形状 | 各功能错误注册表或临时 error 构造函数族 |
RequestContext,含 Params、RoutePattern、RouteName — 仅传输元数据 | 可变请求袋(mutable request bag)、abort 状态或 context service-locator 助手 |
| request ID 与 trace 元数据 carrier | middleware 策略、request-id 生成策略或字段脱敏 |
import "github.com/spcent/plumego/contract"func (h APIHandler) GetUser(w http.ResponseWriter, r *http.Request) { user := fetchUser(r.Context()) contract.WriteResponse(w, r, http.StatusOK, user, nil)}WriteResponse 把 data 包装成 {"data": ..., "request_id": "..."} 并设置 Content-Type: application/json。当 requestid 中间件已运行时,request_id 字段自动填充。
带元数据的分页响应:
contract.WriteResponse(w, r, http.StatusOK, items, map[string]any{ "total": 120, "page": 2,})func (h APIHandler) Greet(w http.ResponseWriter, r *http.Request) { name := r.URL.Query().Get("name") if name == "" { contract.WriteError(w, r, contract.NewErrorBuilder(). Type(contract.TypeRequired). Detail("field", "name"). Message("name is required"). Build()) return } contract.WriteResponse(w, r, http.StatusOK, map[string]string{"message": "hello, " + name}, nil)}调用 Type 会自动设置 HTTP 状态码、错误代码和分类。Message 覆盖默认的人类可读文本。Detail 向 details 映射添加字段。
ErrorType 目录
Section titled “ErrorType 目录”| 常量 | HTTP 状态 | 使用场景 |
|---|---|---|
TypeRequired | 400 | 必要参数或请求体字段缺失 |
TypeValidation | 400 | 字段存在但验证失败 |
TypeInvalidFormat | 400 | 格式错误(UUID、邮箱等) |
TypeOutOfRange | 400 | 值超出允许范围 |
TypeDuplicate | 400 | 值已存在 |
TypeUnauthorized | 401 | 请求未经认证 |
TypeInvalidToken | 401 | Token 格式错误或无效 |
TypeExpiredToken | 401 | Token 已过期 |
TypeForbidden | 403 | 已认证但权限不足 |
TypeNotFound | 404 | 资源不存在 |
TypeConflict | 409 | 状态冲突(如并发更新) |
TypeAlreadyExists | 409 | 资源已存在 |
TypeRateLimited | 429 | 触发限流 |
TypeInternal | 500 | 服务端内部故障 |
TypeUnavailable | 503 | 下游依赖不可用 |
TypeTimeout | 504 | 上游调用超时 |
响应报文格式
Section titled “响应报文格式”每个错误响应都使用以下信封:
{ "error": { "type": "required_field_missing", "code": "REQUIRED", "message": "name is required", "category": "validation_error", "details": { "field": "name" } }, "request_id": "abc-123"}读取 context 助手
Section titled “读取 context 助手”// 由 middleware/requestid 注入的请求 IDrid := contract.RequestIDFromContext(r.Context())
// 路由器设置的路由元数据rc := contract.RequestContextFromContext(r.Context())id := rc.Params["id"]
// 带默认值的查询参数page := r.URL.Query().Get("page")if page == "" { page = "1"}| 错误用法 | 正确模式 |
|---|---|
对错误使用 WriteResponse | 使用 WriteError——它会自动设置正确的 HTTP 状态码 |
WriteError 后忘记 return | 必须立即 return——响应已写入,继续执行会产生未定义行为 |
用结构体字面量构造 APIError | 使用 NewErrorBuilder()——结构体布局不属于稳定 API |
| 在 message 中暴露完整错误字符串 | 服务端记录内部错误;WriteError 中只返回固定的对用户友好的消息 |
所有错误都用 TypeInternal | 选择具体类型:TypeRequired、TypeNotFound 等 |
响应缺少 request_id | 确保 middleware/requestid 在 Prepare 之前注册 |
为什么单独写这一页
Section titled “为什么单独写这一页”contract 是稳定根中允许使用的最低层导入:只有 stdlib。这一约束使它成为系统中每个 handler 共同使用的语言。破坏错误形状或响应信封会造成跨仓库的回归。此处的改动会波及每一个消费者。把 contract 当作已发布的 API 表面,而不是实现细节。
- contract API 快速参考 — 完整函数签名、所有 ErrorType 值和 wire format
- 错误参考 — 所有错误类型含触发场景和修复步骤
- 错误模型
- 错误处理指南
- Core Primer
- Router Primer
- Middleware Primer
- 仓库边界