vault backup: 2026-04-28 08:53:28
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 错误处理, 中间件]
|
||||
create time: 2026-04-28 00:00
|
||||
---
|
||||
|
||||
# 错误处理机制
|
||||
|
||||
## 概述
|
||||
|
||||
Gin 提供了一套链式错误收集 + 全局处理的错误管理机制:handler 中调用 `c.Error(err)` 将错误存入上下文,`c.Errors` 收集所有错误,全局恢复中间件兜底 panic。理解这套机制可以写出结构化、一致的错误响应。
|
||||
|
||||
思考题:如果你的业务函数返回 `(result, err)`,是否需要手动调用 `c.Error(err)`?还是可以直接写 JSON 响应?(详见下文第 2 节)
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. `c.Error` → `c.Errors` 链式错误收集
|
||||
|
||||
Gin 允许在请求生命周期内累积多个错误——这在批量操作或多步验证场景中非常有用:
|
||||
|
||||
```go
|
||||
func batchUpdate(c *gin.Context) {
|
||||
var items []Item
|
||||
c.ShouldBindJSON(&items)
|
||||
|
||||
var errors gin.ErrorList
|
||||
|
||||
for _, item := range items {
|
||||
if err := validate(item); err != nil {
|
||||
// 将错误写入 Context,不会中断后续处理
|
||||
c.Error(&gin.Error{
|
||||
Err: err,
|
||||
Meta: item.ID, // 附加元数据(哪个 item 出错了)
|
||||
})
|
||||
continue
|
||||
}
|
||||
if err := saveToDB(item); err != nil {
|
||||
c.Error(&gin.Error{Err: err, Meta: item.ID})
|
||||
}
|
||||
}
|
||||
|
||||
// 取出所有累积的错误
|
||||
if len(c.Errors) > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"errors": c.Errors.ByType(gin.ErrorTypeBind),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "batch update done"})
|
||||
}
|
||||
```
|
||||
|
||||
**`gin.Error` 结构:**
|
||||
|
||||
```go
|
||||
type Error struct {
|
||||
Err error // 原始错误
|
||||
Type ErrorType // 错误类型
|
||||
Meta interface{} // 附加的任意元数据
|
||||
}
|
||||
|
||||
type ErrorType uint8
|
||||
|
||||
const (
|
||||
ErrorTypeBind ErrorType = 1 << iota // 绑定错误
|
||||
ErrorTypeRelease // 资源释放错误
|
||||
ErrorTypePrivate // 内部业务错误
|
||||
ErrorTypePublic // 暴露给客户端的业务错误
|
||||
)
|
||||
```
|
||||
|
||||
**`c.Errors` 常用方法:**
|
||||
|
||||
| 方法 | 作用 |
|
||||
|------|------|
|
||||
| `c.Errors.ByType(t)` | 按类型过滤错误 |
|
||||
| `c.Errors.Last()` | 获取最后一个错误 |
|
||||
| `c.Errors.First()` | 获取第一个错误 |
|
||||
| `c.Errors.ToStrings()` | 转为字符串切片 |
|
||||
|
||||
> **关键理解:** `c.Error(err)` 不终止请求、不回写响应——它只是往 `c.errors` 切片里追加。你需要显式检查 `len(c.Errors)` 并返回错误响应。
|
||||
|
||||
思考题:`c.Error()` 和直接 `c.JSON(500, ...)` 有什么区别?什么时候应该用前者?
|
||||
|
||||
### 2. 业务错误的正确处理方式
|
||||
|
||||
大多数情况下,handler 中的错误不需要走 `c.Error` 链,而是直接返回错误响应:
|
||||
|
||||
```go
|
||||
func getUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
user, err := userRepository.FindByID(id)
|
||||
if err != nil {
|
||||
// ✅ 推荐:直接返回统一错误响应
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"message": "user not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
// 服务端错误
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "internal server error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"data": user})
|
||||
}
|
||||
```
|
||||
|
||||
**核心原则:**
|
||||
- **单次错误**:直接 `c.JSON(code, ...)` 返回,不经过 `c.Error`
|
||||
- **批量/多步场景**:用 `c.Error` 收集所有错误,一次性返回
|
||||
- **panic 异常**:交给 `gin.Recovery()` 兜底
|
||||
|
||||
### 3. 全局恢复中间件 — `gin.Recovery()`
|
||||
|
||||
默认 `gin.Default()` 已经内置了 `Recovery()` 中间件——它会捕获所有 panic,生成 500 响应,并打印堆栈到日志:
|
||||
|
||||
```go
|
||||
// gin/recovery.go — 简化版
|
||||
func Recovery() HandlerFunc {
|
||||
return func(c *Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 打印堆栈追踪
|
||||
debug.PrintStack()
|
||||
// 写入错误链
|
||||
c.Error(err.(error))
|
||||
// 返回 500
|
||||
c.AbortWithStatus(JSONErrorCode(http.StatusInternalServerError))
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**自定义 Recovery:**
|
||||
|
||||
```go
|
||||
r := gin.New()
|
||||
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||
log.Printf("Panic recovered: %v", recovered)
|
||||
|
||||
// 可以在这里做更多事:发送告警、记录指标等
|
||||
|
||||
if err, ok := recovered.(error); ok {
|
||||
c.JSON(500, gin.H{
|
||||
"code": 500,
|
||||
"message": "internal server error",
|
||||
"trace_id": c.GetString("request-id"),
|
||||
})
|
||||
} else {
|
||||
c.JSON(500, gin.H{
|
||||
"code": 500,
|
||||
"message": "unexpected error",
|
||||
})
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
> **提问:** `gin.Recovery()` 把 panic 也写进了 `c.Errors`,这意味着如果你在 Recovery 之后还有中间件,它们能看到这个错误吗?
|
||||
|
||||
答案:不能。`recover()` 后调用了 `c.AbortWithStatus()`,中间件链已经终止。
|
||||
|
||||
### 4. HTTP 状态码与业务码的映射
|
||||
|
||||
生产环境通常维护两层错误码体系:
|
||||
|
||||
| 层级 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **HTTP 状态码** | 网络层语义 | 200 / 400 / 404 / 500 |
|
||||
| **业务错误码** | 业务语义 | `40001` 参数校验失败 / `40002` 余额不足 |
|
||||
|
||||
```go
|
||||
// 统一错误响应格式
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"` // 业务错误码
|
||||
Message string `json:"message"` // 用户可读消息
|
||||
Trace string `json:"trace,omitempty"` // 调试用 trace ID
|
||||
}
|
||||
|
||||
func handleBizError(c *gin.Context, httpCode, bizCode int, msg string) {
|
||||
c.JSON(httpCode, ErrorResponse{
|
||||
Code: bizCode,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
// 使用
|
||||
handleBizError(c, http.StatusBadRequest, 40001, "用户名不能为空")
|
||||
handleBizError(c, http.StatusNotFound, 40401, "订单不存在")
|
||||
```
|
||||
|
||||
**常见业务码分段约定:**
|
||||
|
||||
| 段 | HTTP 码 | 含义 |
|
||||
|----|---------|------|
|
||||
| 2xxxx | 200 | 成功 |
|
||||
| 40xxx | 400 | 客户端错误(参数、校验) |
|
||||
| 401xx | 401 | 未认证 |
|
||||
| 403xx | 403 | 无权限 |
|
||||
| 404xx | 404 | 资源不存在 |
|
||||
| 50xxx | 500 | 服务端内部错误 |
|
||||
|
||||
思考题:如果你的 API 同时面向前端和第三方开发者,你觉得业务错误码是放在 JSON body 里好,还是也应该加一个自定义响应头(如 `X-Biz-Code`)?为什么?
|
||||
|
||||
### 5. 统一错误响应中间件
|
||||
|
||||
把错误处理抽成中间件,确保所有路由的响应格式一致:
|
||||
|
||||
```go
|
||||
func errorMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// 检查是否有积压的错误
|
||||
if len(c.Errors) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 取最后一个(或最严重)的错误
|
||||
lastErr := c.Errors.Last()
|
||||
switch lastErr.Type {
|
||||
case gin.ErrorTypePrivate:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 50000,
|
||||
"message": "internal server error",
|
||||
})
|
||||
case gin.ErrorTypePublic:
|
||||
if e, ok := lastErr.Err.(*GinError); ok {
|
||||
c.JSON(e.HTTPCode, gin.H{
|
||||
"code": e.Code,
|
||||
"message": e.Message,
|
||||
})
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 50000,
|
||||
"message": "internal server error",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意:** 如果 handler 已经通过 `c.JSON()` 写入了响应体,中间件再写会覆盖或报错。因此需要在 handler 返回前拦截错误,而不是在 `c.Next()` 之后处理。更常见的做法是封装统一的 `c.Success()` / `c.Error()` 辅助方法。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/3-middleware]] — 中间件执行顺序与全局中间件注册
|
||||
- [[GIN/5-binding-validation]] — 参数绑定的错误属于 ErrorTypeBind
|
||||
- [[GIN/observability]] — 错误上报 Prometheus 与链路追踪
|
||||
Reference in New Issue
Block a user