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()
}))
最佳实践总结
- 标准化响应:始终通过
ErrorResponse
返回错误,结构一致。 - 错误分类:用
AppError
区分错误类型(如业务错误、参数错误)。 - 集中处理:使用中间件统一转换错误为响应,避免散布在各处。
- 日志记录:记录原始错误信息(包括堆栈),但生产环境不返回细节。
- 合理拆分状态码:
4xx
:客户端错误(如参数错误 400
、权限不足 403
)5xx
:服务端错误(如数据库故障 500
)
- 处理 Panic:确保服务不崩溃,并记录关键信息。
通过以上实践,错误处理将更系统、可维护,同时提升 API 的健壮性和用户体验。