Go语言中常见100问题-#53-54 Not handling an error & defer errors

2022年4月13日 333点热度 0人点赞 0条评论

不处理错误

在某些时候,我们需要忽略函数的返回值。在Go语言中,应该只有一种处理方法。下面开始分析原因。

下面的notify函数返回一个错误值,我们对返回值不感兴趣,所以直接忽略掉不进行任何处理。

func f() {
        // ...
        notify()
}

func notify() error {
        // ...
}

上面f函数中调用notify函数后,没有将返回值赋值给任何error变量,从语法层面来说,没有任何问题,这段代码是可以通过编译并且是按预期的效果执行。

然而从代码可维护性的角度,这将会导致一些问题。假如一个新程序员在读到这段代码的时候,他会猜测是作者忘记处理notify返回值了呢还是特意忽略它?

所以,在Go语言中,当想忽略函数的返回值时,只有如下的一种写法,将返回的错误值赋值给_,虽然对于编译器来说,这种写法与前面的没有区别,但它显示的告诉程序员不需要处理返回值。

_ = notify()

我们可以在代码的旁边添加注释说明,像下面的注释说明应该避免,因为它没有说明代码不处理返回值的原因,而只是在重复说明代码显示忽略返回值。

// Ignore the error
_ = notify()

合理的注释应该是像下面这样,指明要忽略原因。

// Notifications are sent in best effort.
// Hence, it's accepted to miss some of them in case of errors.
_ = notify()

忽略Go语言中的错误返回值是一种例外的情况,大部分情况下,可以采用日志记录错误的方式处理,即使在较低的日志级别。然而,如果我们确定一个错误可以并且应该被忽略,我们必须通过将它分配给空白标识符来显示处理。这样,将来的读者就会明白这是特意这样处理的。

不处理defer语句错误

不处理defer语句中的错误是Go开发人员经常犯的问题。下面开始讨论原因以及解决方法。

下面的函数是实现一个给定账号ID从数据库中查询余额的功能,我们将使用database/sql中的query方法。具体实现如下,这里只关注查询本身,对结果转换处理不在这里讨论。

const query = "..."

func getBalance(db *sql.DB, clientID string) (
        float32, error)
 {
        rows, err := db.Query(query, clientID)
        if err != nil {
                return 0, err
        }
        defer rows.Close()

        // Use rows
}

rows是一个*sql.Rows类型,它实现了Closer接口方法。

 type Closer interface {
        Close() error
}

上面的接口包含一个Close方法,该方法返回一个error参数。前面讨论了函数的返回errors值总是应该被处理。然而,本例中defer调用返回的错误值却被忽略了。

defer rows.Close()

根据前面讨论的结果,如果我们不想对返回错误值进行处理,需要将它赋值给一个_. 像下面这样。

defer func() { _ = rows.Close() }()

上面这个版本有点冗长,但从可维护的角度来看更好,它准确的反映了我们期望忽略返回值的想法。

然而,在这种情况下与其盲目地忽略defer调用中的返回值,需要问问这是不是最好的处理方法。调用Close()将在无法释放数据库连接时返回错误,因此,忽略这个错误并不是我们想要的,更好的处理方法是记录错误日志。下面的代码,在rows执行Close失败时,会将错误信息记录在日志中,方便我们排查问题。

defer func() {
        err := rows.Close()
        if err != nil {
                log.Printf("failed to close rows: %v", err)
        }
}()

如果,换一种处理方式,现在不处理错误,将错误值返回给getBalance,以便该函数的调用方决定如何处理。代码实现如下:

defer func() {
        err := rows.Close()
        if err != nil {
                return err
        }
}()

上面的这段代码是无法通过编译的,因为匿名函数是没有返回值的,现在返回一个错误是不行的。如何将defer func中的error与getBalance中的返回error建立联系呢,可以采用命名结果参数。代码如下,一旦rows.Close被调用,它的返回值将被赋值给外层的getBalance函数的返回值。

func getBalance(db *sql.DB, clientID string) (
        balance float32, err error)
 {
        rows, err := db.Query(query, clientID)
        if err != nil {
                return 0, err
        }
        defer func() {
                err = rows.Close()
        }()

        if rows.Next() {
                err := rows.Scan(&balance)
                if err != nil {
                        return 0, err
                }
                return balance, nil
        }
        // ...
}

上面这段代码初看是可以的,实际是存在问题的。如果rows.Scan执行失败,rows.Close调用总是被执行。这将导致rows.Close的返回值会覆盖掉rows.Scan返回值。可能会出现,rows.Scan执行失败但rows.Close执行成功,最后返回的错误值为nil, 这并不是我们期望的效果。

上述实现的逻辑并不简单,预期的效果是

rows.Scan rows.Close 返回值
执行成功 执行成功 返回nil
执行成功 执行失败 返回rows.Close的错误
执行失败 执行成功 期望返回rows.Scan的错误
执行失败 执行失败 到底返回哪个错误?

如果rows.Scanrows.Close都执行失败,如何处理呢?有两种不同的处理方法, 方法一:自定义的一个错误类型,包含这种两种错误。方法二:返回rows.Scan错误值,并记录rows.Close错误信息到日志中。方法二实现代码如下

defer func() {
        closeErr := rows.Close()
        if err != nil {
                if closeErr != nil {
                        log.Printf("failed to close rows: %v", closeErr)
                }
                return
        }
        err = closeErr
}()

上述代码将rows.Close的返回值赋值给一个临时变量closeErr,在将closeErr赋值给err之前,检查err值是否是为非nil, 如果err非nil,说明rows.Scan已经出现了错误。这时,不将closeErr赋值给err,直接返回它,并将closeErr的错误信息记录到日志中。

如前面所述,应始终处理错误。对于defer调用返回的错误,我们至少应该明确地忽略它。如果这还不够,我们可以决定直接通过记录错误或将错误传递给调用者来处理错误。

28330Go语言中常见100问题-#53-54 Not handling an error & defer errors

这个人很懒,什么都没留下

文章评论