vault backup: 2026-04-28 19:33:43
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 错误处理, API设计]
|
||||
create time: 2026-04-28 00:15
|
||||
---
|
||||
|
||||
# 业务错误码:JSON Body vs 响应头
|
||||
|
||||
## 概述
|
||||
|
||||
讨论 API 返回业务错误码时,应该放在 JSON body 还是自定义 HTTP 响应头(如 `X-Biz-Code`),以及何时需要两者兼有。
|
||||
|
||||
## 正文
|
||||
|
||||
### 方案对比
|
||||
|
||||
| | JSON Body(`code` 字段) | 响应头(`X-Biz-Code`) |
|
||||
|---|---|---|
|
||||
| **优点** | 解析简单,所有语言/框架天然支持 | 中间件/网关不经解析 body 就能识别错误类型 |
|
||||
| **缺点** | 客户端必须先解析 body 才知道发生了什么 | 不是所有代理层都透传自定义头;某些 SDK 默认不暴露 headers |
|
||||
|
||||
```go
|
||||
// 方案 A:仅 JSON body
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 40001,
|
||||
"message": "参数校验失败",
|
||||
})
|
||||
|
||||
// 方案 B:同时写入 header + body
|
||||
c.Header("X-Biz-Code", strconv.Itoa(40001))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 40001,
|
||||
"message": "参数校验失败",
|
||||
})
|
||||
```
|
||||
|
||||
### 面向前端(同团队)
|
||||
|
||||
JSON body 就够了。前端代码是你写的,统一走 body 的 `code` 字段即可,加 header 反而增加复杂度。
|
||||
|
||||
```typescript
|
||||
// 前端统一拦截器处理
|
||||
axios.interceptors.response.use(null, (err) => {
|
||||
const bizCode = err.response?.data?.code;
|
||||
if (bizCode === 40001) showToast("请检查输入");
|
||||
if (bizCode === 40101) navigateTo("/login");
|
||||
return Promise.reject(err);
|
||||
});
|
||||
```
|
||||
|
||||
### 面向第三方开发者 —— 推荐双通道
|
||||
|
||||
原因有三:
|
||||
|
||||
1. **网关 / 反向代理决策**——WAF、API Gateway 或熔断器通常只能看 HTTP 状态码和 header,无法解析 JSON body。`X-Biz-Code` 能让网关直接拦截特定错误(比如 `40001` 直接限流,而不打到你的服务)。
|
||||
|
||||
2. **SDK 封装友好**——第三方集成 SDK 后,可根据 header 快速区分重试策略:瞬时错误自动重试,确定错误直接抛出,无需先解 body。
|
||||
|
||||
3. **调试效率**——运维通过日志打印 `$http_x_biz_code` 就能一眼看到业务错误分布,不用额外解析请求体。
|
||||
|
||||
### Gin 中的统一实现
|
||||
|
||||
```go
|
||||
func handleBizError(c *gin.Context, httpCode, bizCode int, msg string) {
|
||||
c.Header("X-Biz-Code", strconv.Itoa(bizCode)) // ← header
|
||||
c.JSON(httpCode, gin.H{ // ← body
|
||||
"code": bizCode,
|
||||
"message": msg,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
成本极小——多一行 `c.Header()` 而已,但对两边的使用者(前端 + 第三方)都照顾到了。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/6-error-handling]] — 错误处理机制总览
|
||||
- [[GIN/6-error-handling/q-c-error-nil-panic]] — c.Error(nil) 是否会导致 panic
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 错误处理, FAQ]
|
||||
create time: 2026-04-28 00:15
|
||||
---
|
||||
|
||||
# `c.Error(nil)` 会导致 panic 吗?
|
||||
|
||||
## 概述
|
||||
|
||||
回答一个常见的疑虑:在 Gin 的链式错误收集中,传入 `nil` 作为 `c.Error()` 的参数是否会触发运行时 panic。结论是 **不会 panic**,但会带来逻辑污染问题。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 为什么不会 panic
|
||||
|
||||
`c.Error(err)` 的底层实现只是往内部切片追加元素,不做 nil 检查:
|
||||
|
||||
```go
|
||||
// gin/context.go — 简化版
|
||||
func (c *Context) Error(err error) {
|
||||
c.errors = append(c.errors, &Error{
|
||||
Err: err, // 直接赋值,nil 没问题
|
||||
Type: ErrorTypePrivate,
|
||||
Meta: nil,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Go 的 `append` 对 nil 元素完全安全——它等价于在切片中插入一个零值指针。所以调用 `c.Error(nil)` 不会触发任何 panic。
|
||||
|
||||
### 2. 但它的行为值得警惕
|
||||
|
||||
虽然不 panic,nil 错误会在下游产生两类实际问题:
|
||||
|
||||
#### ① 逻辑污染 —— 静默 Bug
|
||||
|
||||
```go
|
||||
c.Error(nil)
|
||||
c.Error(nil)
|
||||
|
||||
// len(c.Errors) == 2,进入了"有错误"分支
|
||||
if len(c.Errors) > 0 {
|
||||
c.JSON(400, gin.H{"errors": c.Errors.ToStrings()})
|
||||
// → ["<nil>", "<nil>"] ← 客户端收到了无意义的空错误列表
|
||||
}
|
||||
```
|
||||
|
||||
nil 错误会:
|
||||
- 虚增 `len(c.Errors)`,让条件判断走入错误分支
|
||||
- 干扰 `ByType` 过滤结果
|
||||
- 让 `Last()` / `First()` 返回无效值
|
||||
|
||||
#### ② 类型断言场景 —— 通常安全但有边界风险
|
||||
|
||||
```go
|
||||
last := c.Errors.Last()
|
||||
if e, ok := last.Err.(*BizError); ok {
|
||||
// nil 的类型断言结果是 ok == false → 安全
|
||||
}
|
||||
```
|
||||
|
||||
nil 的错误类型的强转结果是 `ok == false`,本身不会 panic。但如果上游的 Recovery 中间件将非 error 类型的 panic 对象塞入 `c.Errors`,再经此处强转就会触发 panic——这是 recovery 链中的假设被违反,并非 `c.Error(nil)` 直接导致,但 nil 错误会让这类边界更难排查。
|
||||
|
||||
### 3. 最佳实践
|
||||
|
||||
在调用前加一层 nil 守卫即可彻底规避:
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
c.Error(&gin.Error{
|
||||
Err: err,
|
||||
Meta: item.ID,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
或者封装统一辅助方法时内置防护:
|
||||
|
||||
```go
|
||||
func ReportError(c *gin.Context, httpCode, bizCode int, msg string) {
|
||||
if msg == "" {
|
||||
return // 空消息不写入错误链
|
||||
}
|
||||
c.Error(&gin.Error{
|
||||
Err: &BizError{Code: bizCode, Message: msg, HTTPCode: httpCode},
|
||||
Type: gin.ErrorTypePrivate,
|
||||
Meta: nil,
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/6-error-handling]] — 错误处理机制总览
|
||||
- [[GIN/3-middleware]] — 中间件执行顺序与 Recovery 链
|
||||
- [[GIN/5-binding-validation]] — 绑定错误的 ErrorTypeBind 分类
|
||||
Reference in New Issue
Block a user