统一用中间件捕获handler返回的error,通过statuscode()接口映射状态码;自定义apperror含code、message、internalmsg三字段;statuscodemap按code查表并fallback到500;gin/echo需桥接且注意recover顺序。

Go HTTP handler里怎么让错误自动转成对应状态码
直接用 http.Error 或手动写 w.WriteHeader + json.NewEncoder 很容易漏状态码、重复写逻辑,而且无法区分业务错误和系统 panic。核心解法是:所有 handler 统一用中间件捕获 error 返回值,再根据 error 类型映射状态码。
实操建议:
- 定义一个接口
interface{ StatusCode() int },让自定义 error 实现它 - handler 函数签名统一改成
func(w http.ResponseWriter, r *http.Request) error - 中间件里 recover panic,检查返回 error 是否实现了
StatusCode,没实现就当 500 处理 - 别在 handler 内部调用
http.Error—— 这会干扰中间件的统一响应流程
常见错误现象:panic: interface conversion: error is *json.SyntaxError, not statusCoder,说明你传了标准库 error 进去但没包装;或中间件没正确 recover 导致 500 页面裸露。
自定义 Error 类型该带哪些字段才够用
只带 Error() 方法和 StatusCode() 不够。真实场景要支持日志追踪、前端提示、调试分级。字段必须精简,但不能少这三样:
立即学习“go语言免费学习笔记(深入)”;
-
Code string:业务码(如"user_not_found"),不是数字,方便前后端约定和监控聚合 -
Message string:用户可见提示(如"用户不存在"),默认不暴露敏感信息,可配合 locale 动态替换 -
InternalMsg string:仅日志记录用(如"user_id=123 not found in postgres"),绝不返回给前端
性能影响:字段多不会拖慢 error 创建,但别在里面塞 struct 指针或 map —— 容易引发 GC 压力或意外序列化泄露。错误类型本身应是值类型,比如 type AppError struct { Code, Message, InternalMsg string; Status int }。
HTTP 状态码映射表写在哪最不容易出错
别写死在中间件里,也别散落在各个 handler 中。最佳位置是自定义 error 的 StatusCode() 方法内部,按 Code 字段查表。
- 用
map[string]int静态初始化一次(放在init()或包变量),避免每次调用都 map lookup - 未命中的
Code必须 fallback 到 500,而不是 panic 或忽略 —— 否则线上请求直接 500 却找不到原因 - 别把 400/401/403/404/500 全塞进一个大 switch —— 维护困难,且容易漏新增业务码
示例片段:
func (e *AppError) StatusCode() int {
if s, ok := statusCodeMap[e.Code]; ok {
return s
}
return http.StatusInternalServerError
}其中 statusCodeMap 是包级 var,key 是 e.Code,不是 error 类型名。
中间件如何兼容标准库 handler 和 Gin/Echo 路由
Go 标准库 http.Handler 是函数式接口,Gin/Echo 是方法式,强行统一抽象层反而增加心智负担。务实做法是:只封装标准库风格的中间件,Gin/Echo 用各自方式桥接。
- 对标准库:写
func(http.Handler) http.Handler,在ServeHTTP里调用你的 handler 函数并处理 error 返回值 - 对 Gin:用
gin.HandlerFunc包一层,c.Next()后检查c.Errors或自定义c.Get("error") - 对 Echo:用
echo.MiddlewareFunc,在next(c)后读c.Response().Status或自定义上下文 key
容易踩的坑:recover() 在 Gin/Echo 中可能被框架自身 recover 掉,导致你的中间件收不到 panic;解决办法是确保你的中间件注册顺序在框架默认 recover 之前(Gin 要早于 gin.Recovery())。
真正难的是 error 的生命周期管理 —— 从 DB 层抛出、经 service 封装、到 handler 映射状态码、再到日志打点和 Sentry 上报,每个环节都可能改写 message 或丢掉 internalMsg。字段命名、构造方式、传递路径,三者必须严格一致,否则线上查问题时看到的永远是“something went wrong”。










