【问题标题】:How to gracefully handle errors in web service如何优雅地处理 Web 服务中的错误
【发布时间】:2018-11-10 17:25:06
【问题描述】:

我正在使用 gin 编写一个简单的 REST API。我已经阅读了许多关于在 go 中减少错误处理重复性的帖子和文本,但我似乎无法完全理解如何在 gin 处理程序中执行此操作。

我的服务所做的只是对数据库运行一些查询并将结果作为 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”模式四次。在基于“选择”的 API 中,我有两倍的 API,因为在绑定输入数据时可能会出现错误,而在将响应编组为 JSON 时会出现错误。有没有好办法让这个更干?

【问题讨论】:

  • 这是设计使然。 Go 很冗长。您可以像 Adrian 那样提取逻辑,但没有什么神奇的结构可以避免 if err != nil
  • 似乎有很多关于使用 golang 错误的接口特性和管理中间件中 Web 服务的错​​误处理的建议,但我不确定如何将这些应用于 gin。跨度>
  • 没有一个很好的方法来处理中间件中的错误。处理程序不会返回错误,因此唯一的方法是恐慌/恢复,这是一种处理此类错误的可怕方法。
  • 我有点想知道为什么在 go 社区中不赞成恐慌/恢复。不使用的原因是什么?
  • @MadWombat:阅读this。但是 TL;DR; 异常为错误(或 Go 中的恐慌)是一种解决过时语言限制的方法,不适用于 Go(或许多其他现代语言)。跨度>

标签: go go-gin


【解决方案1】:

您可以使用助手使其稍微更干燥:

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 行,但它确实抽象掉了重复的逻辑,允许您将重复的错误处理更改为一个处理错误的地方而不是任何地方(例如,如果您想添加错误日志记录,或更改错误响应等)

【讨论】:

  • 这有点好,虽然我还是更喜欢更干燥的东西。如果没有更好的结果,我会标记你的答案。
  • 不幸的是,惯用的 Go 包含检查每个可能返回错误的调用的错误模式。
【解决方案2】:

我通常的方法是使用包装函数。这样做的好处是(相对于 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 的版本和我的版本)中,我的版本更具可读性,原因很简单,错误以完全惯用的方式处理,并且它们冒泡到顶部处理程序。如果您的控制器最终调用其他返回错误的函数,则同样的方法也同样有效。通过将所有错误处理移至最顶层函数,您可以在所有其余代码中摆脱错误处理混乱(除了简单的 'if err != nil { return err }` 构造)。

同样值得注意的是,这种方法可以与 Adrian 的强大结合,特别是与多个处理程序一起使用时,通过更改“包装”功能:

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

【讨论】:

  • 对于这种特殊情况,这是一个很好的模式,我的目标是更通用的“一个必须处理大量潜在错误的处理程序”。我想说,鉴于deleteAPI 现在实际上是一个数据库处理程序,它应该只将sql.DB 作为参数而不是依赖于gin。这样它就可以被转移到一个根本不需要处理 gin 的 DBAL 包中,只需要处理数据库。
  • @Adrian:我认为您的处理程序方法非常强大——我经常使用它,并且经常与这个方法结合使用。我现在更新了我的答案,将组合作为一个选项包含在内。
  • 是的,我倾向于同时使用这两种方法——一个使用错误帮助程序的 HTTP 处理程序,它调用一个 DBAL,将多个可能的 DB 错误合并到一个返回值中。我有时还会在 DBAL 中使用另一个错误助手,它将特定于 DB 的错误转换为内部错误,这样 HTTP 层就不需要知道特定于 DB 的错误类型。
  • 不要为忘记检查错误的人优化。这样的人应该遇到他们遇到的问题:)(但说真的……编写代码以适应那些明确编写糟糕代码的人是一个可怕的想法。不检查错误是任何 Go 编码器所能做的最糟糕的事情之一。)
  • @MadWombat 有很多 Go 代码,包括在标准库中,连同错误一起返回一个非 nilable 值(想到无处不在的 Read/Write 方法)。在错误的同时返回一个不可空值并没有什么不好或脆弱的。不仅看起来,而且实际上很糟糕,其他 Go 程序员没有检查错误。
猜你喜欢
  • 2015-05-15
  • 1970-01-01
  • 2020-03-19
  • 1970-01-01
  • 1970-01-01
  • 2021-03-11
  • 1970-01-01
  • 2021-05-26
  • 2017-11-10
相关资源
最近更新 更多