Files
Inbox/Go项目实战/03_基础设施/01_错误处理/Phase 1 统一响应结构定义 (The Contract).md

146 lines
4.2 KiB
Markdown
Raw Permalink Normal View History

2025-12-11 07:24:36 +08:00
---
tags: []
aliases:
- 📦 统一响应结构定义 (The Contract)
date created: 星期三, 十二月 10日 2025, 11:23:15 上午
date modified: 星期三, 十二月 10日 2025, 12:12:46 中午
---
# Phase 1 统一响应结构定义 (The Contract)
## 📦 统一响应结构定义 (The Contract)
所有 HTTP 接口(无论成功与否)必须严格返回以下 JSON 结构:
```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
```JSON
{
"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 API`0` 明确表示逻辑执行成功。
- **Data 类型:** 返回具体的 Object。
---
### 🟡 场景 B: 成功返回空列表 (Empty List)
请求: GET /api/v1/articles?category=golang (假设该分类下无文章)
HTTP Status: 200 OK
```JSON
{
"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)
```JSON
{
"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 对象。
```JSON
{
"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`,则透传其 `msg`Case C-1
-`err` 是普通 `error`Case C-2视为系统级异常。**必须**将 JSON 中的 `msg` 强制重写为 `"Internal Server Error"` 或通用文案防止数据库表结构、IP 地址等敏感信息泄露给攻击者。
- **HTTP 200:** 即使是 Code 50000HTTP Status 依然保持 200。这确保了网关层Nginx/Gateway不会拦截 Body前端始终能解析 JSON 拿到 `code``trace_id` 用于展示和报错。
---
## 🛡️ 关键实现逻辑预告
为了实现上述契约,在接下来的 **Step 5: `internal/pkg/app/response.go`** 中,我们将实现如下核心逻辑:
1. **Trace ID 注入:**`New(c)` 时执行 `r.traceID = c.GetString("trace_id")`
2. **错误清洗:**
```Go
// 伪代码逻辑预览
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** 的所有契约要求。