Files
Inbox/Go项目实战/03_基础设施/01_错误处理/Phase 1 统一响应结构定义 (The Contract).md
2025-12-11 07:24:36 +08:00

4.2 KiB
Raw Blame History

tags, aliases, date created, date modified
tags aliases date created date modified
📦 统一响应结构定义 (The Contract)
星期三, 十二月 10日 2025, 11:23:15 上午 星期三, 十二月 10日 2025, 12:12:46 中午

Phase 1 统一响应结构定义 (The Contract)

📦 统一响应结构定义 (The Contract)

所有 HTTP 接口(无论成功与否)必须严格返回以下 JSON 结构:

{
  "code": 20001,           // 业务状态码 (0=成功, 非0=错误)
  "msg": "用户已存在",       // 用户可见的提示文案 (Safe Message)
  "data": { ... },         // 业务数据 payload (成功时返回,失败时通常为 null)
  "trace_id": "a1b2-c3d4"  // 全链路追踪 ID (必填,用于 SRE 排查)
}

🎨 场景示例与设计理由

🟢 场景 A: 成功返回对象 (Single Object)

请求: GET /api/v1/users/1001

HTTP Status: 200 OK

{
  "code": 0,
  "msg": "OK",
  "data": {
    "user_id": 1001,
    "nickname": "TechLead_01",
    "avatar": "https://cdn.example.com/u/1001.jpg"
  },
  "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
}

📌 设计理由:

  • Code 0: 符合业界惯例(如 Google/Tencent API0 明确表示逻辑执行成功。
  • Data 类型: 返回具体的 Object。

🟡 场景 B: 成功返回空列表 (Empty List)

请求: GET /api/v1/articles?category=golang (假设该分类下无文章)

HTTP Status: 200 OK

{
  "code": 0,
  "msg": "OK",
  "data": {
    "list": [],
    "total": 0
  },
  "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
}

📌 设计理由:

  • Data 不为 null: 对于列表型接口,data 内部的 list 字段必须返回空数组 [],而不是 null
    • 原因: 前端可以直接调用 .map().forEach() 而无需判空,极大降低前端出现 Cannot read property 'map' of null 的崩溃风险。
  • 结构一致性: 即使是列表,建议包裹在 Object 中(如 {list: [], total: 0}),方便未来扩展分页字段。

🔴 场景 C: 业务/系统错误 (Error Handling)

这里我们需要区分 “预期内的业务错误”“预期外的系统错误”,但在 JSON 表现上它们必须是一致的。

Case C-1: 预期内的业务错误

场景: 用户尝试注册已存在的邮箱。

Service 层返回: ecode.UserAlreadyExist (Code: 20001)

{
  "code": 20001,
  "msg": "用户已存在",
  "data": null,
  "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
}

Case C-2: 预期外的系统错误 (触发安全防御)

场景: 数据库突然断连GORM 返回 dial tcp 127.0.0.1:5432: connect: connection refused。

Service 层返回: 原生 error 对象。

{
  "code": 50000,
  "msg": "Internal Server Error",  // <--- 严禁显示 "dial tcp ..."
  "data": null,
  "trace_id": "0a1b2c3d-4e5f-6789-1234-567890abcdef"
}

📌 设计理由:

  • Security (安全降级):
    • app.Error(err) 的内部逻辑必须执行 Type Assertion (类型断言)
    • err*ecode.Error,则透传其 msgCase C-1
    • err 是普通 errorCase C-2视为系统级异常。必须将 JSON 中的 msg 强制重写为 "Internal Server Error" 或通用文案防止数据库表结构、IP 地址等敏感信息泄露给攻击者。
  • HTTP 200: 即使是 Code 50000HTTP Status 依然保持 200。这确保了网关层Nginx/Gateway不会拦截 Body前端始终能解析 JSON 拿到 codetrace_id 用于展示和报错。

🛡️ 关键实现逻辑预告

为了实现上述契约,在接下来的 Step 5: internal/pkg/app/response.go 中,我们将实现如下核心逻辑:

  1. Trace ID 注入:New(c) 时执行 r.traceID = c.GetString("trace_id")
  2. 错误清洗:
// 伪代码逻辑预览
func (r *Response) Error(err error) {
    if e, ok := err.(*ecode.Error); ok {
        // 业务错误:直接使用
        r.json(e.Code(), e.Msg())
    } else {
        // 系统错误:记录原始日志,但在 JSON 中降级
        log.Error("System Error", zap.Error(err), zap.String("trace_id", r.traceID))
        r.json(ecode.ServerErr, "Internal Server Error") // 🔒 安全替换
    }
}

这套设计已经满足了 Phase 1 的所有契约要求。