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

Go 如何优雅地处理web服务中的错误,go,go-gin,Go,Go Gin,我正在使用gin在go中编写一个简单的restapi。我读过很多关于在围棋中减少错误处理重复性的帖子和文章,但我似乎无法集中精力在如何在围棋中实现这一点 我的服务所做的就是对数据库运行一些查询,并将结果作为JSON返回,因此典型的处理程序如下所示 func DeleteAPI(c *gin.Context) { var db = c.MustGet("db").(*sql.DB) query := "DELETE FROM table WHERE some condition"

我正在使用gin在go中编写一个简单的restapi。我读过很多关于在围棋中减少错误处理重复性的帖子和文章,但我似乎无法集中精力在如何在围棋中实现这一点

我的服务所做的就是对数据库运行一些查询,并将结果作为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!=零模式四次。在基于选择的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行,但会抽象出重复逻辑,允许您在一个位置而不是在处理错误的任何地方更改重复的错误处理,例如,如果要添加错误日志记录或更改错误响应,等。

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

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的答案有优势,Adrian的答案也是一个很好的答案,顺便说一句,将错误处理保留在返回结果err的更为惯用的形式中,而不是将代码与HandleErrorer类型调用混在一起,同时仍将其整合到一个位置

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程序员来说,优先考虑的是代码的可读性而不是干性。在我看来,在你的原版、阿德里安的和我的三个选项中,我的版本更具可读性,原因很简单,错误的处理方式完全是惯用的,它们会冒泡到顶级处理程序。如果您的控制器最终调用了其他返回错误的函数,那么这种方法也同样有效。通过将所有错误处理移到最顶层的函数,您可以摆脱除简单的“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})
}

我通常的方法是使用包装函数。这比Adrian的答案有优势,Adrian的答案也是一个很好的答案,顺便说一句,将错误处理保留在返回结果err的更为惯用的形式中,而不是将代码与HandleErrorer类型调用混在一起,同时仍将其整合到一个位置

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程序员来说,优先考虑的是代码的可读性而不是干性。在我看来,在你的原版、阿德里安的和我的三个选项中,我的版本更具可读性,原因很简单,错误的处理方式完全是惯用的,它们会冒泡到顶级处理程序。如果您的控制器最终调用了其他返回错误的函数,那么这种方法也同样有效。通过将所有错误处理移到最顶层的函数,您可以摆脱除简单的“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})
}

这是故意的。围棋很冗长。您可以像Adrian那样提取逻辑,但是如果出现错误,没有一个神奇的构造可以避免关于使用golang错误的接口性质和在中间件中管理web服务的错误处理,似乎有很多建议,但我不确定如何将这些应用到gin中。在中间件中处理错误的方法不是很好。处理程序无法返回错误,因此唯一的方法是使用panic/recover,这是一种处理此类错误的糟糕方法。我有点想知道为什么在围棋社区中panic/recover不受欢迎。不使用它的原因是什么?@MadWombat:读一读。但是TL;博士Go中的错误或恐慌等异常是过时语言限制的一种解决办法,这不适用于Go或许多其他现代语言。围棋很冗长。您可以像Adrian那样提取逻辑,但是如果出现错误,没有一个神奇的构造可以避免关于使用golang错误的接口性质和在中间件中管理web服务的错误处理,似乎有很多建议,但我不确定如何将这些应用到gin中。在中间件中处理错误的方法不是很好。处理程序无法返回错误,因此唯一的方法是panic/recover,这是一种糟糕的处理方法
像这样的错误。我有点奇怪为什么恐慌/恢复在围棋社区是不受欢迎的。不使用它的原因是什么?@MadWombat:读一读。但是TL;博士Go中的错误或恐慌等异常是过时语言限制的一个解决办法,这不适用于Go或许多其他现代语言。这稍微好一点,尽管我还是更喜欢更干燥的语言。如果没有更好的答案,我会标记你的答案。不幸的是,惯用的围棋包括为每个可能返回错误的呼叫检查错误的模式。这是一个更好的模式,尽管我仍然喜欢更干燥的东西。如果没有更好的方法,我会标记你的答案。不幸的是,惯用的Go包括为每个可能返回错误的调用检查错误的模式。对于这种特殊情况,这是一个很好的模式,我的目标是为处理大量潜在错误的处理程序提供更通用的模式。我想说,鉴于deleteAPI现在实际上是一个数据库处理程序,它应该将sql.DB作为一个参数,而不是依赖于gin。这样,它就可以被移动到一个DBAL包中,而不需要处理gin,只需要处理数据库。@Adrian:我认为你的处理程序方法非常强大——我经常使用它,并且经常与这个方法结合使用。我现在更新了我的答案,将组合作为一个选项。是的,我倾向于使用两者-一个使用错误助手的HTTP处理程序,它调用一个DBAL,将多个可能的DB错误合并为一个返回值。有时,我还会在DBAL中使用另一个错误助手,将特定于DB的错误转换为内部错误,这样HTTP层就不需要知道特定于DB的错误类型。并且不要为忘记检查错误的人进行优化。这样的人应该得到他们所遇到的问题:但说真的。。。编写代码以适应那些明确编写糟糕代码的人是一个糟糕的想法。而不检查错误是任何Go编码器所能做的最糟糕的事情之一。@MadWombat有很多Go代码,包括在标准库中,这些代码与错误一起还返回一个不可为零的值。我们想到了无处不在的读/写方法。返回错误旁边的不可为零的值并没有什么不好或脆弱的地方。这不仅看起来是错误,实际上是不好的,是其他Go程序员没有检查错误。对于这种特殊情况,这是一个很好的模式,我的目标是为处理大量潜在错误的处理程序提供更通用的模式。我想说,鉴于deleteAPI现在实际上是一个数据库处理程序,它应该将sql.DB作为一个参数,而不是依赖于gin。这样,它就可以被移动到一个DBAL包中,而不需要处理gin,只需要处理数据库。@Adrian:我认为你的处理程序方法非常强大——我经常使用它,并且经常与这个方法结合使用。我现在更新了我的答案,将组合作为一个选项。是的,我倾向于使用两者-一个使用错误助手的HTTP处理程序,它调用一个DBAL,将多个可能的DB错误合并为一个返回值。有时,我还会在DBAL中使用另一个错误助手,将特定于DB的错误转换为内部错误,这样HTTP层就不需要知道特定于DB的错误类型。并且不要为忘记检查错误的人进行优化。这样的人应该得到他们所遇到的问题:但说真的。。。编写代码以适应那些明确编写糟糕代码的人是一个糟糕的想法。而不检查错误是任何Go编码器所能做的最糟糕的事情之一。@MadWombat有很多Go代码,包括在标准库中,这些代码与错误一起还返回一个不可为零的值。我们想到了无处不在的读/写方法。返回错误旁边的不可为零的值并没有什么不好或脆弱的地方。不仅表面上看起来很糟糕,实际上很糟糕的是,其他Go程序员没有检查错误。