跳转到内容

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())
}
  • 你正在编写或修改 WriteErrorWriteResponse 的输出形状
  • 你正在添加或修改 error category 或 error type
  • 你正在读写请求级别的元数据,例如 request ID、route params 或 trace headers
  • 改动关心的是 route matching 或 URL parameter 提取策略 — 那属于 router
  • 改动引入了 session lifecycle、token 签发或 auth identity — 那属于 security
  • 工作关于 middleware 如何观察或转换请求 — 从 middleware 开始
  • 改动添加了 protocol-gateway 行为、multipart upload 便捷函数或特定功能的响应信封
  1. contract/module.yaml
  2. contract/response.go
  3. contract/errors.go
  4. contract/context_core.go
  5. reference/standard-service/internal/handler/health.go
这些工作适合留在 contract一旦变成这些问题就应移出
WriteError / WriteResponse — 唯一规范的写入路径带有各功能构造函数的特定功能响应信封
ErrorCategoryErrorType 常量与 APIError 形状各功能错误注册表或临时 error 构造函数族
RequestContext,含 ParamsRoutePatternRouteName — 仅传输元数据可变请求袋(mutable request bag)、abort 状态或 context service-locator 助手
request ID 与 trace 元数据 carriermiddleware 策略、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 覆盖默认的人类可读文本。Detaildetails 映射添加字段。

常量HTTP 状态使用场景
TypeRequired400必要参数或请求体字段缺失
TypeValidation400字段存在但验证失败
TypeInvalidFormat400格式错误(UUID、邮箱等)
TypeOutOfRange400值超出允许范围
TypeDuplicate400值已存在
TypeUnauthorized401请求未经认证
TypeInvalidToken401Token 格式错误或无效
TypeExpiredToken401Token 已过期
TypeForbidden403已认证但权限不足
TypeNotFound404资源不存在
TypeConflict409状态冲突(如并发更新)
TypeAlreadyExists409资源已存在
TypeRateLimited429触发限流
TypeInternal500服务端内部故障
TypeUnavailable503下游依赖不可用
TypeTimeout504上游调用超时

每个错误响应都使用以下信封:

{
"error": {
"type": "required_field_missing",
"code": "REQUIRED",
"message": "name is required",
"category": "validation_error",
"details": { "field": "name" }
},
"request_id": "abc-123"
}
// 由 middleware/requestid 注入的请求 ID
rid := 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选择具体类型:TypeRequiredTypeNotFound
响应缺少 request_id确保 middleware/requestidPrepare 之前注册

contract 是稳定根中允许使用的最低层导入:只有 stdlib。这一约束使它成为系统中每个 handler 共同使用的语言。破坏错误形状或响应信封会造成跨仓库的回归。此处的改动会波及每一个消费者。把 contract 当作已发布的 API 表面,而不是实现细节。