应使用 apphandler 替代标准 http.handler,让 handler 返回 error 并由包装器统一处理 json 错误响应,避免 http.error 破坏结构、goroutine 错误丢失及状态码语义丢失。

别用 http.Error 直接返回错误页面,它强制文本/HTML、无法嵌入 code 或 trace_id,会破坏你定的 JSON 错误结构。
为什么自定义错误页面总被忽略或返回 200
常见现象:handler 返回了 errors.New("not found"),但前端收到 200 空响应。根本原因是 Go 的 http.Handler 接口不接受 error 返回值,错误没被任何地方捕获就“消失”了。
- 标准库 handler 签名是
func(http.ResponseWriter, *http.Request),没法把 error 向上传递 - 如果你在 goroutine 里出错(比如
go db.Query()),中间件完全收不到——它只管主请求协程 - 没做类型断言就直接调
http.Error(w, err.Error(), 500),会导致非StatusError类型的 error 被当成字符串处理,丢失状态码语义
用 AppHandler 替换原始 handler 类型
核心是让 handler 自己能返回 error,再由包装器统一处理。这不是加个中间件就行,而是改写 handler 定义方式:
type AppHandler func(http.ResponseWriter, *http.Request) error
func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
switch e := err.(type) {
case *StatusError:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
http.Error(w, fmt.Sprintf(`{"code":%d,"message":"%s"}`, e.Status, html.EscapeString(e.Err.Error())), e.Status)
default:
w.Header().Set("Content-Type", "application/json; charset=utf-8")
http.Error(w, `{"code":500,"message":"internal server error"}`, http.StatusInternalServerError)
}
return
}
}
- 所有业务 handler 改成返回
error,例如func home(w http.ResponseWriter, r *http.Request) error { ... } - 注册路由时必须用
AppHandler(home)包一层,否则编译不通过 -
StatusError必须用指针接收器实现Error()方法,否则类型断言失败
中间件里 recover panic 但别吐堆栈给前端
panic 不等于业务错误,中间件要区分对待:前者需记录日志并返回通用 500,后者应走 StatusError 流程。
- 中间件中
defer func() { if r := recover(); r != nil { log.Printf("PANIC: %+v", r) ... } }()是基础,但恢复后不能直接调http.Error—— 可能已写过 header - 更安全的做法是:recover 后设置一个标记,让外层
AppHandler.ServeHTTP统一响应,避免多次写 response - 别在 recover 里调
r.Body.Close()或读r.FormValue,此时r可能已失效 - 如果用了 Gin,必须用
gin.RecoveryWithWriter()并配合c.AbortWithStatusJSON(),默认gin.Recovery()不支持 JSON 响应
错误结构体字段导出和 JSON 序列化陷阱
json.Marshal 对零值和 nil 指针的处理很敏感,稍不注意就返回 {"code":0,"message":""} 这种无效错误。
- 结构体字段必须首字母大写(导出),且带
json:tag,例如Code int `json:"code"`,否则序列化为空对象 - 避免用
*string字段存 message:未初始化时会输出"message": null,而前端通常期望字符串 - 状态码字段建议设默认值:
Code int = http.StatusInternalServerError,防止传入 0 导致 HTTP 协议错误 - 不要在错误结构里嵌套未判空的
error字段,e.Err.Error()在e.Err == nil时 panic
最易被忽略的一点:goroutine 内部错误永远进不了中间件或 AppHandler,必须在启动 goroutine 时显式传入 context 并用 channel 或回调上报错误,否则它只会静默失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











