go http handler中需用defer+recover统一拦截panic并转为json错误,优先使用框架自定义recovery中间件,业务error应实现apierror接口以映射状态码和错误码,请求id须在入口注入context并透传至响应,避免responsewriter多次写入。

Go HTTP handler 中如何统一拦截 panic 并转为 JSON 错误
panic 会直接终止 handler 执行并返回空白响应或 500 页面,前端收不到结构化错误。必须用 recover() 拦住,再手动写入标准 JSON 错误体。
- 所有顶层 handler(如
http.HandleFunc或 Gin 的c.Next()前)都得包一层defer+recover() - 恢复后别直接
log.Fatal,要调用你封装的错误响应函数,比如writeJSONError(w, http.StatusInternalServerError, "internal_error", "server panic") - Gin/Echo 等框架已有中间件机制,优先用
gin.Recovery(),但默认不输出 JSON;需自定义 recovery 中间件,替换掉原http.Error()调用 - 注意:recover 只对当前 goroutine 有效,异步 goroutine(如
go func(){}())里的 panic 不会被捕获
error 类型怎么映射到 HTTP 状态码和错误码字符串
不能靠 err.Error() 字符串匹配来判断类型——太脆弱。应该让业务 error 实现自定义接口,携带状态码和 code 字段。
- 定义接口:
type APIError interface { Error() string; StatusCode() int; Code() string } - 业务层主动返回实现该接口的 struct,比如
&ValidationError{Msg: "email invalid", Field: "email"} - 中间件中用
errors.As(err, &target)判断是否是APIError,再取target.StatusCode()和target.Code() - 别把数据库错误(如
"pq: duplicate key")直接透出给前端,要兜底转换成通用错误码如"resource_conflict"
JSON 错误响应结构要不要包含 trace_id 或 request_id
要,但不能硬编码生成,也不能全靠日志库自动注入。必须在请求进入时就生成并透传到错误响应里。
- 用中间件在
context.Context中塞入request_id,比如ctx = context.WithValue(r.Context(), ctxKeyRequestID, id) - 错误响应结构体里留一个
RequestID字段,写响应时从r.Context().Value()里取 - 别用
uuid.New().String()在错误构造时临时生成——会导致日志和响应中的 ID 不一致 - 如果用了 OpenTelemetry,优先从
trace.SpanFromContext(r.Context()).SpanContext().TraceID()提取,更利于链路追踪对齐
为什么不能在 defer 里直接 json.Marshal 错误再写入 ResponseWriter
因为 ResponseWriter 可能已被部分写入(比如之前调用了 w.WriteHeader(200)),此时再写 JSON 会触发 http: multiple response.WriteHeader calls 错误。
立即学习“go语言免费学习笔记(深入)”;
- 必须在写任何 body 前就决定最终状态码,错误处理逻辑应尽早介入,而不是等最后才“补救”
- 推荐做法:用包装过的
ResponseWriter(如responseWriterWrapper),记录是否已写 header/body,错误发生时先检查、再清理已写内容(仅限开发环境)或直接 panic(生产环境不该走到这步) - 更稳妥方案:所有 handler 返回
error,由顶层路由层统一判断并写响应,避免中途写入 - Gin 用户注意:
c.AbortWithStatusJSON()是安全的,它会自动处理 header 冲突;但别在defer里调它,而应在 recover 后显式调用
真正难的不是结构定义,而是让每个业务 error 都老老实实实现接口、每条中间件都记得透传 context、每次异步操作都意识到 panic 不会被捕获——这些地方一漏,标准就塌一角。










