Web 错误处理
问题
Go Web 服务中如何设计统一的错误处理和响应格式?
答案
统一响应格式
// 统一响应结构
type Response struct {
Code int `json:"code"` // 业务状态码
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 数据
}
func Success(c *gin.Context, data interface{}) {
c.JSON(200, Response{Code: 0, Message: "success", Data: data})
}
func Fail(c *gin.Context, httpCode int, bizCode int, msg string) {
c.JSON(httpCode, Response{Code: bizCode, Message: msg})
}
业务错误码设计
// 错误码定义
type AppError struct {
HTTPCode int `json:"-"` // HTTP 状态码
Code int `json:"code"` // 业务错误码
Message string `json:"message"`
}
func (e *AppError) Error() string { return e.Message }
// 预定义错误
var (
ErrNotFound = &AppError{HTTPCode: 404, Code: 40400, Message: "资源不存在"}
ErrUnauthorized = &AppError{HTTPCode: 401, Code: 40100, Message: "未授权"}
ErrForbidden = &AppError{HTTPCode: 403, Code: 40300, Message: "无权限"}
ErrBadRequest = &AppError{HTTPCode: 400, Code: 40000, Message: "参数错误"}
ErrInternal = &AppError{HTTPCode: 500, Code: 50000, Message: "服务器错误"}
)
// 带自定义消息
func NewAppError(base *AppError, msg string) *AppError {
return &AppError{HTTPCode: base.HTTPCode, Code: base.Code, Message: msg}
}
错误处理中间件
func ErrorMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// 处理 c.Error() 收集的错误
if len(c.Errors) == 0 {
return
}
err := c.Errors.Last().Err
var appErr *AppError
if errors.As(err, &appErr) {
c.JSON(appErr.HTTPCode, Response{
Code: appErr.Code,
Message: appErr.Message,
})
} else {
// 未知错误:记录日志,返回通用错误
log.Printf("未处理错误: %v", err)
c.JSON(500, Response{
Code: 50000,
Message: "服务器内部错误",
})
}
}
}
// 在 Handler 中使用
func getUser(c *gin.Context) {
user, err := userService.FindByID(c.Param("id"))
if err != nil {
_ = c.Error(err) // 收集错误,交给中间件统一处理
c.Abort()
return
}
Success(c, user)
}
Recovery 中间件
处理 Handler 中的 panic:
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
// 记录堆栈
stack := debug.Stack()
log.Printf("PANIC: %v\n%s", r, stack)
c.AbortWithStatusJSON(500, Response{
Code: 50000,
Message: "服务器内部错误",
})
}
}()
c.Next()
}
}
注意
生产环境不要将内部错误信息(如 SQL 错误、堆栈)返回给客户端,只记录到日志中。
常见面试问题
Q1: 为什么不在每个 Handler 里直接 c.JSON(500, ...)?
答案:统一错误处理的好处:
- 一致性:所有错误响应格式统一
- 解耦:业务代码只关心返回 error,展示逻辑在中间件
- 日志集中:在中间件统一记录错误日志
- 易维护:修改错误格式只改一处
Q2: panic 和 error 在 Web 服务中怎么选?
答案:
- error:业务可预期的错误(查不到数据、参数错误),必须用 error
- panic:不可恢复的程序错误(空指针、越界),配合 Recovery 中间件兜底
Go Web 服务中 99% 的场景用 error,Recovery 只是最后防线。