良好的错误响应格式能提升 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 设计的重要部分。定义清晰的错误码和错误消息,配合中间件自动处理,可以让代码更加整洁。验证错误要格式化成友好的提示,方便前端展示。