统一错误响应

API 的错误响应格式一致,对前端开发和 API 使用者都很友好。这一章介绍如何在 Gin 中实现统一的错误响应。

统一响应结构

基本结构

// 成功响应
type SuccessResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

// 错误响应
type ErrorResponse struct {
    Code    int               `json:"code"`
    Message string            `json:"message"`
    Errors  []ValidationError `json:"errors,omitempty"`
}

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

响应辅助函数

// 成功响应
func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, SuccessResponse{
        Code:    0,
        Message: "success",
        Data:    data,
    })
}

// 创建成功
func Created(c *gin.Context, data interface{}) {
    c.JSON(http.StatusCreated, SuccessResponse{
        Code:    0,
        Message: "created",
        Data:    data,
    })
}

// 错误响应
func Fail(c *gin.Context, code int, message string) {
    c.JSON(code, ErrorResponse{
        Code:    code,
        Message: message,
    })
}

// 参数验证错误
func FailWithErrors(c *gin.Context, errors []ValidationError) {
    c.JSON(http.StatusBadRequest, ErrorResponse{
        Code:    http.StatusBadRequest,
        Message: "validation failed",
        Errors:  errors,
    })
}

完整示例

package main

import (
    "net/http"
    
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)

// 响应结构
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
    Errors  interface{} `json:"errors,omitempty"`
}

// 响应码
const (
    CodeSuccess = 0
    CodeError   = 1
)

// 快捷响应方法
func OK(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    CodeSuccess,
        Message: "success",
        Data:    data,
    })
}

func Created(c *gin.Context, data interface{}) {
    c.JSON(http.StatusCreated, Response{
        Code:    CodeSuccess,
        Message: "created",
        Data:    data,
    })
}

func NoContent(c *gin.Context) {
    c.Status(http.StatusNoContent)
}

func BadRequest(c *gin.Context, message string) {
    c.JSON(http.StatusBadRequest, Response{
        Code:    CodeError,
        Message: message,
    })
}

func Unauthorized(c *gin.Context, message string) {
    c.JSON(http.StatusUnauthorized, Response{
        Code:    CodeError,
        Message: message,
    })
}

func Forbidden(c *gin.Context, message string) {
    c.JSON(http.StatusForbidden, Response{
        Code:    CodeError,
        Message: message,
    })
}

func NotFound(c *gin.Context, message string) {
    c.JSON(http.StatusNotFound, Response{
        Code:    CodeError,
        Message: message,
    })
}

func InternalError(c *gin.Context, message string) {
    c.JSON(http.StatusInternalServerError, Response{
        Code:    CodeError,
        Message: message,
    })
}

func ValidationError(c *gin.Context, err error) {
    var errors []map[string]string
    
    if verrs, ok := err.(validator.ValidationErrors); ok {
        for _, verr := range verrs {
            errors = append(errors, map[string]string{
                "field":   verr.Field(),
                "message": getValidationMessage(verr),
            })
        }
    }
    
    c.JSON(http.StatusBadRequest, Response{
        Code:    CodeError,
        Message: "validation failed",
        Errors:  errors,
    })
}

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

// 用户结构
type CreateUserRequest struct {
    Name     string `json:"name" binding:"required,min=2,max=50"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

func main() {
    r := gin.New()
    r.Use(gin.Recovery())
    
    // 用户路由
    users := r.Group("/users")
    {
        users.GET("/:id", getUser)
        users.POST("", createUser)
        users.PUT("/:id", updateUser)
        users.DELETE("/:id", deleteUser)
    }
    
    r.Run(":8080")
}

func getUser(c *gin.Context) {
    id := c.Param("id")
    
    user, err := findUser(id)
    if err != nil {
        NotFound(c, "用户不存在")
        return
    }
    
    OK(c, user)
}

func createUser(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        ValidationError(c, err)
        return
    }
    
    user, err := createUserService(req)
    if err != nil {
        InternalError(c, "创建用户失败")
        return
    }
    
    Created(c, user)
}

func updateUser(c *gin.Context) {
    id := c.Param("id")
    
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        ValidationError(c, err)
        return
    }
    
    user, err := updateUserService(id, req)
    if err != nil {
        NotFound(c, "用户不存在")
        return
    }
    
    OK(c, user)
}

func deleteUser(c *gin.Context) {
    id := c.Param("id")
    
    if err := deleteUserService(id); err != nil {
        NotFound(c, "用户不存在")
        return
    }
    
    NoContent(c)
}

// 模拟服务
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func findUser(id string) (*User, error) {
    return &User{ID: 1, Name: "Alice", Email: "alice@example.com"}, nil
}

func createUserService(req CreateUserRequest) (*User, error) {
    return &User{ID: 1, Name: req.Name, Email: req.Email}, nil
}

func updateUserService(id string, req CreateUserRequest) (*User, error) {
    return &User{ID: 1, Name: req.Name, Email: req.Email}, nil
}

func deleteUserService(id string) error {
    return nil
}

分页响应

type PageResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
    Meta    PageMeta    `json:"meta"`
}

type PageMeta struct {
    Page      int   `json:"page"`
    PageSize  int   `json:"page_size"`
    Total     int64 `json:"total"`
    TotalPage int   `json:"total_page"`
}

func PageOK(c *gin.Context, data interface{}, page, pageSize int, total int64) {
    totalPage := int(total) / pageSize
    if int(total)%pageSize > 0 {
        totalPage++
    }
    
    c.JSON(http.StatusOK, PageResponse{
        Code:    CodeSuccess,
        Message: "success",
        Data:    data,
        Meta: PageMeta{
            Page:      page,
            PageSize:  pageSize,
            Total:     total,
            TotalPage: totalPage,
        },
    })
}

// 使用
func listUsers(c *gin.Context) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
    
    users, total := getUserList(page, pageSize)
    
    PageOK(c, users, page, pageSize, total)
}

中间件方式

func responseMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        // 如果响应为空,返回默认响应
        if c.Writer.Status() == 0 || c.Writer.Size() == 0 {
            OK(c, nil)
        }
    }
}

小结

统一错误响应的好处:

  • API 响应格式一致,便于前端处理
  • 错误信息结构化,便于调试
  • 代码复用,减少重复
  • 易于扩展和维护

建议在项目初期就确定好响应格式,并封装好响应辅助函数,让整个团队的代码风格保持一致。