Gin中间件错误处理方法

How to handle errors in Gin middleware

我想抓取每条路由上的所有 http 错误,而不是每次都重写 if 400 then if 404 then if 500 then etc...所以我在每个路由处理程序中都有一个 ErrorHandler() 函数:

func (h *Handler) List(c *gin.Context) {
    movies, err := h.service.ListService()

    if err != nil {
        utils.ErrorHandler(c, err)
        return
    }

    c.JSON(http.StatusOK, movies)
}

这个函数看起来像这样:

func ErrorHandler(c *gin.Context, err error) {
    if err == ErrNotFound {
        // 404
        c.JSON(http.StatusNotFound, gin.H{"error": ErrNotFound.Error()})
    } else if err == ErrInternalServerError {
        // 500
        c.JSON(http.StatusInternalServerError, gin.H{"error": ErrInternalServerError.Error()})
    } // etc...
}

ErrNotFoundErrInternalServerError 只是像这样初始化的全局变量:

var ErrNotFound = errors.New(http.StatusText(http.StatusNotFound))  // 404

我想知道我做的是否正确,或者是否有更好的方法来做到这一点,比如抓住中间件中的错误并 return 直接响应?

使用 node.js 我能够在中间件参数中发送 err 并像这样使用它:

app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
    if (err instanceof HttpError) {
        res.status(err.status).json({error: err.message});
      } else if (err instanceof Error) {
        res.status(500).json({error: err.message});
      } else {
        res.status(500).send("Internal Server Error");
      }
});

有类似的东西吗?

比使用函数更惯用(utils 作为包名也不受欢迎)是使用中间件:

func ErrorHandler(c *gin.Context) {
        c.Next()

        for _, err := range c.Errors {
            // log, handle, etc.
        }
    
        c.JSON(http.StatusInternalServerError, "")
}


func main() {
    router := gin.New()
    router.Use(middleware.ErrorHandler)
    // ... routes
}

值得注意的是,您在 中间件 func 之前调用 c.Next() 您的实际错误处理代码,因此您确保错误处理发生在处理程序链的其余部分之后已调用。

Next should be used only inside middleware. It executes the pending handlers in the chain inside the calling handler. [...]

使用中间件的好处是你还可以向它传递参数,例如一个记录器,您可能希望稍后将其用作错误处理的一部分,一次,而不是每次直接调用 utils.ErrorHandler 时都传递它。在这种情况下,它看起来像这样(我使用 Uber Zap 记录器):

func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        for _, ginErr := range c.Errors {
            logger.Error("whoops", ...)
        }
    }
}

func main() {
    router := gin.New()
    
    logger, _ := zap.NewDevelopment()

    router.Use(middleware.ErrorHandler(logger))
    // ... routes
}

然后处理程序将中止链,而不是调用一个函数,这看起来更干净并且更容易维护:

func (h *Handler) List(c *gin.Context) {
    movies, err := h.service.ListService()

    if err != nil {
        c.AbortWithError(http.StatusInternalServerError, err)
        return
    }

    c.JSON(http.StatusOK, movies)
}

请务必注意,如果您在 c.AbortWithStatusc.AbortWithError 中设置 HTTP 状态,您可能希望 在错误处理程序中覆盖它.在这种情况下,您可以使用 -1 作为状态代码调用 c.JSON()

func ErrorHandler(logger *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        for _, ginErr := range c.Errors {
            logger.Error("whoops", ...)
        }

        // status -1 doesn't overwrite existing status code
        c.JSON(-1, /* error payload */)
    }
}

最后,使用中间件允许您在处理程序中多次调用 c.Error,例如当发生一系列非致命错误并且您想在实际中止请求之前捕获所有这些错误时。

Error attaches an error to the current context. The error is pushed to a list of errors. It's a good idea to call Error for each error that occurred during the resolution of a request. A middleware can be used to collect all the errors and [process them]

func (h *Handler) List(c *gin.Context) {
    err1 := /* non-fatal error */
    if err1 != nil {
        c.Error(err1)
    }

    err2 := /* another non-fatal error */
    if err2 != nil {
        c.Error(err2)
    }

    fatalErr := /* fatal error */
    if fatalErr != nil {
        c.AbortWithError(505, fatalErr)
        return
        // the error handler will have collected all 3 errors
    }

    c.JSON(http.StatusOK, movies)
}

至于中间件中的实际错误处理,它非常简单。请记住,所有对 c.Errorc.AbortWith... 的调用都会将您的错误包装在 gin.Error 中。因此,要检查原始值,您必须检查 err.Err 字段:

func ErrorHandler(c *gin.Context) {
        c.Next()

        for _, err := range c.Errors {
            switch err.Err {
                case ErrNotFound:
                  c.JSON(-1, gin.H{"error": ErrNotFound.Error()})  
            }
            // etc...
        }

        c.JSON(http.StatusInternalServerError, "")
}

迭代 c.Errors 可能看起来很笨拙,因为现在您可能有 N 个错误而不是一个,但是根据您打算如何使用中间件,您可以简单地检查 len(c.Errors) > 0 并仅访问第一项 c.Errors[0].