如何处理 Gin 中间件中的错误

Joh*_*ohn 15 error-handling go go-gin

我想获取每个路由上的所有 http 错误,而无需每次重写 if 400 then if 404 then if 500 then 等等...所以我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)
}
Run Code Online (Sandbox Code Playgroud)

这个函数看起来像这样:

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...
}
Run Code Online (Sandbox Code Playgroud)

ErrNotFound或者ErrInternalServerError只是像这样初始化的全局变量:

var ErrNotFound = errors.New(http.StatusText(http.StatusNotFound))  // 404
Run Code Online (Sandbox Code Playgroud)

我想知道我做得是否正确,或者是否有更好的方法来做到这一点,例如捕获中间件内的错误并直接返回响应?

使用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");
      }
});
Run Code Online (Sandbox Code Playgroud)

有类似的东西吗?

bla*_*een 48

比使用函数(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
}
Run Code Online (Sandbox Code Playgroud)

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

Next 只能在中间件内部使用。它执行调用处理程序内链中的待处理处理程序。[...]

使用中间件的优点是您还可以向它传递参数,例如记录器,您可能希望稍后将其用作错误处理的一部分,一次,而不是每次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
}
Run Code Online (Sandbox Code Playgroud)

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

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)
}
Run Code Online (Sandbox Code Playgroud)

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

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 */)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

错误将错误附加到当前上下文。该错误被推送到错误列表中。对于请求解析过程中发生的每个错误,调用 Error 是一个好主意。中间件可用于收集所有错误并[处理它们]

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)
}
Run Code Online (Sandbox Code Playgroud)

至于中间件中的实际错误处理,非常简单。请记住,所有对c.Error,的调用c.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, "")
}
Run Code Online (Sandbox Code Playgroud)

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