如何优雅地处理 Web 服务中的错误

How to gracefully handle errors in web service

我正在使用 gin 在 go 中编写一个简单的 REST API。我已经阅读了很多关于在 go 中减少错误处理重复性的帖子和文本,但我似乎无法理解如何在 gin 处理程序中做到这一点。

我的服务所做的只是 运行 一些针对数据库的查询和 return 结果 JSON,因此典型的处理程序如下所示

func DeleteAPI(c *gin.Context) {
    var db = c.MustGet("db").(*sql.DB)
    query := "DELETE FROM table WHERE some condition"
    tx, err := db.Begin()
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    defer tx.Rollback()
    result, err := tx.Exec(query)
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    num, err := result.RowsAffected()
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    err = tx.Commit()
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, gin.H{"deleted": num})
}

如您所见,即使是这个简单的处理程序也会重复相同的 "if err != nil" 模式四次。在基于 "select" 的 API 中,我有两倍的数量,因为在绑定输入数据时存在潜在错误,而在将响应编组为 JSON 时存在潜在错误。有什么好的方法可以让它更干吗?

你可以 稍微 借助助手让它更干:

func handleError(c *gin.Context, err error) bool {
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return true
    }
    return false
}

用作:

err = tx.Commit()
if handleError(c,err) {
    return
}

这只会将错误处理行数从 4 行减少到 3 行,但它确实抽象掉了重复的逻辑,让您在一个地方而不是在处理错误的所有地方更改重复的错误处理(例如,如果您想添加错误日志记录,或更改错误响应等)

我通常的做法是使用包装函数。这有一个优势(相对于 Adrian 的回答——这也是一个很好的回答,顺便说一句),即以更符合 Go 惯用形式的错误处理(return result, err,而不是用 handleError(err) 类型调用),同时仍将其合并到一个位置。

func DeleteAPI(c *gin.Context) {
    num, err := deleteAPI(c)
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, gin.H{"deleted": num})
}

func deleteAPI(c *gin.Context) (int, error) {
    var db = c.MustGet("db").(*sql.DB)
    query := "DELETE FROM table WHERE some condition"
    tx, err := db.Begin()
    if err != nil {
        return 0, err
    }
    defer tx.Rollback()
    result, err := tx.Exec(query)
    if err != nil {
        return 0, err
    }
    num, err := result.RowsAffected()
    if err != nil {
        return 0, err
    }
    err = tx.Commit()
    if err != nil {
        return 0, err
    }
    return num, nil
}

对我来说(一般来说,对于 Go 程序员来说),代码的可读性优先于 DRY。在我看来,在这三个选项(您的原始版本、Adrian 的和我的)中,我的版本更具可读性,原因很简单,错误以完全惯用的方式处理,并且它们冒泡到顶级处理程序。如果您的控制器最终调用其他 return 错误的函数,同样的方法也同样有效。通过将所有错误处理移至最顶层函数,您可以在所有其余代码中摆脱错误处理混乱(除了简单的“if err != nil { return err }` 构造)。

同样值得注意的是,这种方法可以与 Adrian 的方法结合使用,特别是用于多个处理程序,方法是将 "wrapping" 函数更改为:

func DeleteAPI(c *gin.Context) {
    result, err := deleteAPI(c)
    if handleError(c, err) {
        return
    }
    c.JSON(200, gin.H{"deleted": num})
}