应封装统一错误响应函数并定义httperror接口,强制所有错误路径经writeerror输出结构化json;状态码需语义对齐,code用大写下划线,details过滤敏感信息;前端须双层校验response.status与data.code。

Go HTTP handler 中怎么统一返回错误结构
前端要的不是 500 Internal Server Error,而是带 code、message、details 的 JSON。硬写 json.NewEncoder(w).Encode() 容易漏状态码、错 Content-Type、重复写错误分支。
实操建议:http.Error() 不能用——它只发纯文本;必须手动控制 w.WriteHeader() + json.Encoder。封装一个 writeError() 函数,强制要求所有错误路径走它:
func writeError(w http.ResponseWriter, status int, code string, msg string, details interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]interface{}{
"code": code,
"message": msg,
"details": details,
})
}
- 状态码必须和业务语义对齐:
400(参数校验失败)、401(未登录)、403(无权限)、404(资源不存在)、422(语义错误,如邮箱已注册)、500(服务端不可恢复错误) -
code字段别用中文或空格,推荐全大写下划线风格,比如"USER_NOT_FOUND"、"INVALID_PHONE_FORMAT" - 别在
details里塞敏感信息(如数据库错误详情、堆栈),生产环境必须过滤
如何让 Go 错误类型自带 HTTP 状态码和业务码
每次 if err != nil { writeError(...); return } 太啰嗦,而且状态码散落在各处,改起来容易漏。需要让错误本身“知道”该返回什么。
实操建议:定义一个接口,比如 HTTPError,让自定义错误实现它:
立即学习“go语言免费学习笔记(深入)”;
type HTTPError interface {
error
StatusCode() int
ErrorCode() string
ErrorMessage() string
}
- 所有业务错误(如用户不存在、参数非法)都包装成实现该接口的 struct,例如
UserNotFoundError - handler 里统一用类型断言判断:
if he, ok := err.(HTTPError); ok { writeError(w, he.StatusCode(), he.ErrorCode(), he.ErrorMessage(), nil) } - 别忘了给标准库错误(如
json.UnmarshalTypeError)也包一层,否则会落到500降级处理
前端怎么可靠解析 Go 后端的错误响应
后端返回了结构化错误,但前端如果只看 response.status,会把 400 和 500 都当网络失败处理;如果只信 data.code,又可能忽略 HTTP 层异常(比如网关超时返回 504 却没 JSON body)。
实操建议:前端 fetch 必须同时检查两个层面:
- 先看
response.ok === false或response.status >= 400—— 这表示 HTTP 层已出问题,此时不能直接.json(),得先.text()防止解析失败抛错 - 再尝试解析 JSON,但要有 fallback:
try { data = await res.json() } catch { data = { code: "PARSE_ERROR", message: "Invalid response" } } - 业务逻辑只信任
data.code做分支,不依赖response.status做功能判断(比如不能用status === 401触发登出,而应检查data.code === "UNAUTHORIZED")
为什么中间件里 recover() 捕获 panic 后不能直接用 writeError
panic 发生时,http.ResponseWriter 可能已经写过 header 或部分 body,再调 w.WriteHeader() 会静默失败,前端收到截断或乱码响应。
实操建议:recover 中必须先检测是否已写入:
if f, ok := w.(http.Flusher); ok && !w.Header().Get("Content-Type") == "" {
// 已开始写入,无法挽救,记录日志后直接 return
log.Printf("panic recovered but response already written: %v", r)
return
}
- 更稳妥的做法是用
ResponseWriter包装器(wrapper),重写WriteHeader和Write,在第一次写入前拦截 panic - 所有中间件里的
defer recover()必须放在最外层 handler 执行前,否则可能漏捕获路由匹配后的 panic - 别在 recover 里试图重试或修复状态 —— panic 是程序异常,不是业务错误,不该掩盖
真正难的不是封装结构,而是让每个错误路径都经过同一出口;最容易被绕开的是中间件 panic 捕获和前端双层校验逻辑,这两处一松动,整个规范就失效。










