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)
}
}
}
统一错误响应的好处:
建议在项目初期就确定好响应格式,并封装好响应辅助函数,让整个团队的代码风格保持一致。