golang gin 框架错误处理最佳实践

发布时间: 更新时间: 总字数:952 阅读时间:2m 作者: IP上海 分享 网址

1. 统一错误响应格式

定义标准化的错误响应结构,方便客户端解析:

type ErrorResponse struct {
    Code    int         `json:"code"`    // 自定义业务错误码或 HTTP 状态码
    Message string      `json:"message"` // 用户友好提示
    Details interface{} `json:"details,omitempty"` // 可选调试信息(生产环境隐藏)
}

2. 自定义错误类型

封装错误信息,携带状态码、消息和底层错误:

type AppError struct {
    HTTPCode int       // HTTP 状态码(如 404)
    Code     int       // 业务错误码(可选,用于多维度分类)
    Message  string    // 客户端展示的消息
    Err      error     // 内部错误日志(原始错误,如数据库错误)
}

func (e *AppError) Error() string {
    return e.Message
}

// 构造函数(简化错误创建)
func NewAppError(httpCode, code int, message string, err error) *AppError {
    return &AppError{
        HTTPCode: httpCode,
        Code:     code,
        Message:  message,
        Err:      err,
    }
}

3. 集中式错误处理中间件

编写全局中间件统一处理错误,避免代码重复:

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next() // 先处理请求后续逻辑

        // 检查所有收集的错误
        for _, err := range c.Errors {
            switch e := err.Err.(type) {
            case *AppError: // 自定义业务错误
                log.Printf("AppError: %s (HTTP %d), Internal: %v", e.Message, e.HTTPCode, e.Err)
                c.JSON(e.HTTPCode, ErrorResponse{
                    Code:    e.Code,
                    Message: e.Message,
                    Details: getErrorDetails(c, e.Err), // 根据环境返回细节
                })
            default: // 未知错误或未处理的错误类型
                log.Printf("Unhandled error: %v", e)
                c.JSON(http.StatusInternalServerError, ErrorResponse{
                    Code:    http.StatusInternalServerError,
                    Message: "Internal server error",
                    Details: getErrorDetails(c, e),
                })
            }
            return // 处理首个错误(或遍历所有错误)
        }
    }
}

// 根据环境(开发/生产)决定是否暴露错误细节
func getErrorDetails(c *gin.Context, err error) interface{} {
    if gin.Mode() == gin.DebugMode {
        return err.Error()
    }
    return nil
}

在路由中注册中间件:

router := gin.Default()
router.Use(ErrorHandler())

4. 在业务逻辑中抛出错误

在处理请求时,返回带上下文的 AppError

// 用户路由处理函数示例
func GetUser(c *gin.Context) {
    id := c.Param("id")
    user, err := database.FindUser(id)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            // 明确处理“未找到”错误
            c.Error(NewAppError(http.StatusNotFound, 1001, "User not found", err))
        } else {
            // 通用数据库错误
            c.Error(NewAppError(http.StatusInternalServerError, 1002, "Database error", err))
        }
        c.Abort() // 终止后续处理
        return
    }
    c.JSON(http.StatusOK, user)
}

5. 处理参数绑定错误

使用 ShouldBind 时自动校验参数,失败时返回可读错误:

type UserRequest struct {
    Name  string `json:"name" binding:"required"`
    Email string `json:"email" binding:"required,email"`
}

func CreateUser(c *gin.Context) {
    var req UserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        // 捕获参数校验错误,返回 400 状态码
        c.Error(NewAppError(
            http.StatusBadRequest,
            2001,
            "Invalid request data",
            err,
        ))
        c.Abort()
        return
    }
    // 处理合法请求...
}

6. 自定义 404 处理

统一未匹配路由的错误响应:

router.NoRoute(func(c *gin.Context) {
    c.Error(NewAppError(
        http.StatusNotFound,
        3001,
        "Endpoint not found",
        nil,
    ))
})

7. 处理 Panic 异常

启用内置的 Recovery 中间件,防止进程崩溃:

router.Use(gin.CustomRecovery(func(c *gin.Context, err interface{}) {
    log.Printf("Panic recovered: %v", err)
    c.Error(NewAppError(
        http.StatusInternalServerError,
        5001,
        "Internal server error",
        fmt.Errorf("panic: %v", err),
    ))
    c.Abort()
}))

最佳实践总结

  1. 标准化响应:始终通过 ErrorResponse 返回错误,结构一致。
  2. 错误分类:用 AppError 区分错误类型(如业务错误、参数错误)。
  3. 集中处理:使用中间件统一转换错误为响应,避免散布在各处。
  4. 日志记录:记录原始错误信息(包括堆栈),但生产环境不返回细节。
  5. 合理拆分状态码
    • 4xx:客户端错误(如参数错误 400、权限不足 403
    • 5xx:服务端错误(如数据库故障 500
  6. 处理 Panic:确保服务不崩溃,并记录关键信息。

通过以上实践,错误处理将更系统、可维护,同时提升 API 的健壮性和用户体验。

Home Archives Categories Tags Statistics
本文总阅读量 次 本站总访问量 次 本站总访客数