在某些时候,我们需要忽略函数的返回值。在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语句中的错误是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.Scan
和rows.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调用返回的错误,我们至少应该明确地忽略它。如果这还不够,我们可以决定直接通过记录错误或将错误传递给调用者来处理错误。