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

146 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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** 的所有契约要求。