bigywater.tech
Gin · 27

c.Error 与 c.Errors 的收集

handler 里遇到错误,是直接 c.JSON 返回,还是 c.Error 交给中间件?Gin 的 c.Errors 到底收集什么、Private 和 Public 有什么区别、框架自己会往里放什么?这篇把这些细节试出来,再写一个只暴露 Public 错误的统一处理中间件。

3 条
两次 c.Error + 一次包装后的 c.Error,Errors 里的条数(同一个 *Error 出现了 2 次)
1<<63
c.Bind 失败时放进 c.Errors 的类型(ErrorTypeBind);ShouldBind 失败不放
2 条
含 1 个 Public、2 个 Private 错误时,访问日志里多出的错误行数

Error 的结构与 c.Error

实测 · 本机执行输出
  errors.go(Gin v1.12.0):
      15 | // ErrorType is an unsigned 64-bit error code as defined in the gin spec.
      16 | type ErrorType uint64
      17 | 
      18 | const (
      19 |     // ErrorTypeBind is used when Context.Bind() fails.
      20 |     ErrorTypeBind ErrorType = 1 << 63
      21 |     // ErrorTypeRender is used when Context.Render() fails.
      22 |     ErrorTypeRender ErrorType = 1 << 62
      23 |     // ErrorTypePrivate indicates a private error.
      24 |     ErrorTypePrivate ErrorType = 1 << 0
      25 |     // ErrorTypePublic indicates a public error.
      26 |     ErrorTypePublic ErrorType = 1 << 1
      27 |     // ErrorTypeAny indicates any other error.
      28 |     ErrorTypeAny ErrorType = 1<<64 - 1
      29 | )
  context.go 的 Error(Gin v1.12.0):
     252 | func (c *Context) Error(err error) *Error {
     253 |     if err == nil {
     254 |         panic("err is nil")
     255 |     }
     256 | 
     257 |     var parsedError *Error
     258 |     ok := errors.As(err, &parsedError)
     259 |     if !ok {
     260 |         parsedError = &Error{
     261 |             Err:  err,
     262 |             Type: ErrorTypePrivate,
     263 |         }
     264 |     }
     265 | 
     266 |     c.Errors = append(c.Errors, parsedError)
     267 |     return parsedError
     268 | }

一个 *gin.Error 由底层 Err、类型 Type(位标志)和任意 Meta 组成。c.Error 先用 errors.As 找一个已有的 *gin.Error(所以 %w 包装过也认得),找不到才新建一个 Private 的。返回的指针可以继续 SetType / SetMeta。传 nil 会 panic。

Errors 的各种读取方式

实测 · 本机执行输出
    c.Error(普通 error) → Type=1(Private=1) Meta=<nil>,同一个指针存进 Errors: true
    c.Error(fmt.Errorf("%w", *gin.Error)) → 返回的指针就是原来的 *gin.Error: true;Type=2(Public=2)
    len(c.Errors)=3(同一个 *Error 被存了 2 次)
    Last().Error()="用户名已存在"
    ByType(Public) 有 2 条,ByType(Private) 有 1 条,ByType(Any) 有 3 条
    Errors() = ["数据库超时" "用户名已存在" "用户名已存在"]
    String():
      Error #01: 数据库超时
      Error #02: 用户名已存在
           Meta: map[code:1001]
      Error #03: 用户名已存在
           Meta: map[code:1001]
    MarshalJSON() = [{"error":"数据库超时"},{"code":1001,"error":"用户名已存在"},{"code":1001,"error":"用户名已存在"}]
    只有一个 Private 时 JSON() 是对象 {"error":"数据库超时"}
    c.Error(nil) → panic: err is nil

ByType 按位标志过滤,Last 取最后一条,Errors() 得到字符串切片,String() 得到“Error #01 …”样式的多行文本,JSON() 一条时是对象、多条时是数组,Meta 里的字段会摊平进对象。注意同一个指针 c.Error 两次就会出现两次。

框架自己放什么

实测 · 本机执行输出
    c.Bind 失败                          Errors=1 [Type=9223372036854775808] IsAborted=true 状态=400 已写出=true
    c.ShouldBind 失败                    Errors=0 [] IsAborted=false 状态=200 已写出=false
    c.JSON 序列化失败(channel)              Errors=1 [Type=1] IsAborted=true 状态=200 已写出=false
    ErrorTypeBind=1<<63=9223372036854775808 ErrorTypeRender=1<<62=4611686018427387904(c.Render 失败走的是 c.Error(err),类型是 Private;Render 常量没有被 Gin 自己用到)

c.Bind 系列(MustBindWith)失败时用 AbortWithError(400, err).SetType(ErrorTypeBind),所以既写了 400 又终止了链;ShouldBind 系列不碰 c.Errors,由你决定怎么处理。渲染(如 c.JSON 序列化 channel)失败时用普通的 c.Error 并 Abort,状态还没写出。ErrorTypeRender 这个常量在源码里没有被使用。

统一错误处理

实测 · 本机执行输出
    GET /pub    → 400 {"errors":{"error":"password too short","field":"password"}}
    GET /priv   → 500 {"error":"服务器内部错误"}
    GET /ok     → 200 fine
    GET /early  → 200 已经写了

常见写法:handler 只负责 c.Error(...) 并 return,在最外层中间件的 Next 之后统一写响应。要点有三个:先判断 c.Writer.Written(),别在已经写出响应的情况下再写;只把 Public 类型展示给客户端;Private 错误留给日志,客户端只看固定文案,这样数据库 DSN 之类的内部信息不会泄漏。

Logger 与 Private 错误

实测 · 本机执行输出
    "[GIN] 2026/10/11 - 18:48:30 | 200 |  5.083µs |       192.0.2.1 | GET      \"/x\""
    "Error #01: 私有错误 A"
    "Error #02: 私有错误 C"

Gin 的 Logger 在访问日志后面追加所有 Private 错误(c.Errors.ByType(ErrorTypePrivate).String()),Public 错误不会出现在日志里——所以想让某个错误既展示给客户端又进日志,需要自己在中间件里记录。

分步演示

错误收集
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest 直接调用 ServeHTTP;Logger 的输出用 gin.LoggerWithWriter 写进 bytes.Buffer;每种情形各一次
    • 没有验证:Error.Meta 为结构体或指针时 JSON() 的输出;errors.Join 的多错误处理;自定义 LoggerFormatter 下 ErrorMessage 的使用;Public 错误里放内部信息的泄漏风险(只是设计建议)
    京ICP备2026067502号-1京公网安备11010502063487号