错误响应

良好的错误响应格式能提升 API 的可用性,方便前端处理和问题排查。

统一错误格式

定义统一的错误响应结构:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type ErrorResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Details interface{} `json:"details,omitempty"`
}

func Error(c *gin.Context, code int, message string, details ...interface{}) {
    response := ErrorResponse{
        Code:    code,
        Message: message,
    }
    if len(details) > 0 {
        response.Details = details[0]
    }
    c.JSON(code, response)
}

func main() {
    r := gin.Default()
    
    r.GET("/error", func(c *gin.Context) {
        Error(c, http.StatusBadRequest, "参数错误", gin.H{
            "field": "id",
            "reason": "不能为空",
        })
    })
    
    r.Run(":8080")
}

预定义错误码

const (
    CodeSuccess         = 0
    CodeBadRequest      = 400
    CodeUnauthorized    = 401
    CodeForbidden       = 403
    CodeNotFound        = 404
    CodeConflict        = 409
    CodeInternalError   = 500
    CodeServiceUnavailable = 503
)

var ErrorMessages = map[int]string{
    CodeSuccess:         "成功",
    CodeBadRequest:      "请求参数错误",
    CodeUnauthorized:    "未授权",
    CodeForbidden:       "禁止访问",
    CodeNotFound:        "资源不存在",
    CodeConflict:        "资源冲突",
    CodeInternalError:   "服务器内部错误",
    CodeServiceUnavailable: "服务暂不可用",
}

func APIError(c *gin.Context, code int, details ...interface{}) {
    message, ok := ErrorMessages[code]
    if !ok {
        message = "未知错误"
    }
    
    response := gin.H{
        "code":    code,
        "message": message,
    }
    
    if len(details) > 0 {
        response["details"] = details[0]
    }
    
    c.JSON(http.StatusOK, response)
}

r.GET("/not-found", func(c *gin.Context) {
    APIError(c, CodeNotFound, gin.H{
        "resource": "user",
        "id":       c.Query("id"),
    })
})

业务错误码

const (
    ErrUserNotFound     = 10001
    ErrUserDisabled     = 10002
    ErrPasswordWrong    = 10003
    ErrTokenExpired     = 10004
    ErrTokenInvalid     = 10005
    
    ErrOrderNotFound    = 20001
    ErrOrderPaid        = 20002
    ErrOrderCancelled   = 20003
)

var BizErrors = map[int]string{
    ErrUserNotFound:    "用户不存在",
    ErrUserDisabled:    "用户已被禁用",
    ErrPasswordWrong:   "密码错误",
    ErrTokenExpired:    "登录已过期",
    ErrTokenInvalid:    "无效的登录凭证",
    
    ErrOrderNotFound:   "订单不存在",
    ErrOrderPaid:       "订单已支付",
    ErrOrderCancelled:  "订单已取消",
}

func BizError(c *gin.Context, code int) {
    message, ok := BizErrors[code]
    if !ok {
        message = "业务处理失败"
    }
    
    c.JSON(http.StatusOK, gin.H{
        "code":    code,
        "message": message,
    })
}

r.GET("/user/:id", func(c *gin.Context) {
    user := getUser(c.Param("id"))
    if user == nil {
        BizError(c, ErrUserNotFound)
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "code":    0,
        "message": "success",
        "data":    user,
    })
})

错误处理中间件

func ErrorMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            err := c.Errors.Last()
            
            switch e := err.Err.(type) {
            case *BizException:
                c.JSON(http.StatusOK, gin.H{
                    "code":    e.Code,
                    "message": e.Message,
                })
            default:
                c.JSON(http.StatusInternalServerError, gin.H{
                    "code":    500,
                    "message": "服务器内部错误",
                })
            }
        }
    }
}

type BizException struct {
    Code    int
    Message string
}

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

func Throw(c *gin.Context, code int, message string) {
    c.Error(&BizException{Code: code, Message: message})
}

r.Use(ErrorMiddleware())

r.GET("/test", func(c *gin.Context) {
    Throw(c, ErrUserNotFound, "用户不存在")
})

验证错误格式化

import "github.com/go-playground/validator/v10"

func FormatValidationErrors(err error) []gin.H {
    var errors []gin.H
    
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, e := range validationErrors {
            errors = append(errors, gin.H{
                "field":   e.Field(),
                "message": getErrorMessage(e),
            })
        }
    }
    
    return errors
}

func getErrorMessage(e validator.FieldError) string {
    switch e.Tag() {
    case "required":
        return "此字段为必填项"
    case "email":
        return "邮箱格式不正确"
    case "min":
        return "长度不能少于" + e.Param()
    case "max":
        return "长度不能超过" + e.Param()
    default:
        return "验证失败"
    }
}

r.POST("/validate", func(c *gin.Context) {
    var req struct {
        Name  string `json:"name" binding:"required,min=3"`
        Email string `json:"email" binding:"required,email"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "code":    400,
            "message": "参数验证失败",
            "errors":  FormatValidationErrors(err),
        })
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
})

分级错误日志

func LogError(c *gin.Context, err error, level string) {
    requestID, _ := c.Get("requestID")
    
    entry := log.Printf("[%s] [%s] %s %s - %v",
        requestID,
        level,
        c.Request.Method,
        c.Request.URL.Path,
        err,
    )
    
    switch level {
    case "error":
        log.Println("[ERROR]", entry)
    case "warn":
        log.Println("[WARN]", entry)
    default:
        log.Println("[INFO]", entry)
    }
}

r.GET("/error-log", func(c *gin.Context) {
    err := errors.New("测试错误")
    LogError(c, err, "error")
    
    c.JSON(500, gin.H{"error": "内部错误"})
})

小结

统一的错误响应格式是 API 设计的重要部分。定义清晰的错误码和错误消息,配合中间件自动处理,可以让代码更加整洁。验证错误要格式化成友好的提示,方便前端展示。