7. 处理错误

未匹配的标注

几乎所有使用 database/sql 类型的操作都将错误作为最后一个值返回。您应该始终检查这些错误,永远不要忽略它们。

在一些地方,错误行为是特殊情况,或者您可能需要了解一些额外信息。

来自迭代结果集产生的错误

考虑以下代码:

for rows.Next() {
    // ...
}
if err = rows.Err(); err!= nil {
    // 在这里处理错误
}

来自 rows.Err() 的错误可能是 rows.Next() 循环中各种错误的结果。除了正常完成循环外,循环可能出于其他原因退出,因此您始终需要检查循环是否正常终止。异常终止会自动调用 rows.Close(),尽管多次调用无害。

来自关闭结果集产生的错误

如前所述,如果过早退出循环,则应始终显式关闭sql.Rows。如果循环正常退出或由于错误退出,它将自动关闭,但您可能会错误地这样做:

for rows.Next() {
    // ...
     break;  // 糟糕,行未关闭!内存泄漏...
}
// 执行通常的 "if err = rows.Err()" [此处省略] ...
// 在这里 [re?] 关闭总是安全的:
if err = rows.Close(); err != nil {
    // 但是如果出现错误该怎么办?
    log.Println(err)
}

rows.Close() 返回的错误是常规规则的唯一例外,该常规规则是最好捕获并检查所有数据库操作中的错误。如果 rows.Close() 返回一个错误,则不清楚应该做什么。记录错误消息或 panicing 可能是唯一明智的选择,如果这不明智,那么也许您应该忽略该错误。

来自 QueryRow() 的错误

考虑以下获取单行的代码:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil  {
    log.Fatal(err)
}
fmt.Println(name)

如果没有 id = 1 的用户怎么办?那么结果中就没有行了,而且 .Scan() 也不会将值扫描到 name 中。那会发生什么呢?

Go 定义了一个特殊的错误常量,称为 sql.ErrNoRows,当结果为空时从 QueryRow() 返回该常量。在大多数情况下,这需要作为特殊情况进行处理。应用程序代码通常不会将空结果视为错误,如果不检查错误是否为这个特殊常量,就会导致意想不到的应用程序代码错误。

来自查询的错误会被推迟到调用 Scan() 时,然后再返回。上面的代码最好这样写:

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
    if err == sql.ErrNoRows {
        //没有行,但也没有错误发生
    } else {
        log.Fatal(err)
    }
}
fmt.Println(name)

有人可能会问,为什么将空结果集视为错误。空集没有任何错误。原因是 QueryRow() 方法需要使用此特殊情况,以便让调用者区分 QueryRow() 是否真的找到了一行;如果没有它,Scan() 将无法执行任何操作,您可能不会意识到您的变量根本没有从数据库中获得任何值。

只有在使用 QueryRow() 时才会遇到这个错误。如果您在其他地方遇到此错误,则说明您做错了什么。

识别特定的数据库错误

编写如下代码可能很有诱惑力:

rows, err := db.Query("SELECT someval FROM sometable")
// 错误包含:
// ERROR 1045 (28000):Access denied for user 'foo'@'::1' (using password: NO)
if strings.Contains(err.Error(), "Access denied")  {
    // 处理被拒绝的错误
}

然而,这并不是最好的方法。例如,字符串值可能会有所不同,具体取决于服务器使用哪种语言发送错误消息。最好通过比较错误编号来确定具体的错误是什么。

但是,执行此操作的机制因驱动程序而异,因为它本身不是 database/sql 的一部分。在本教程重点关注的 MySQL 驱动程序中,您可以编写以下代码:

if driverErr, ok := err.(*mysql.MySQLError); ok { // 现在可以直接访问错误编号
    if driverErr.Number == 1045 {
        // 处理被拒绝的错误
    }
}

同样,这里的 MySQLError 类型是由这个特定的驱动程序提供的,而且 .Number 字段可能在不同的驱动程序之间有所不同。然而,该数字的值是从 MySQL 的错误消息中获取的,因此是特定于数据库的,而不是特定于驱动程序的。

这段代码仍然很难看。与 1045(一个魔术般的数字)相比,这是一种代码味道。一些驱动程序(虽然不是 MySQL 驱动程序,但由于一些与本文无关的原因) 提供了错误标识符列表。例如,Postgres pq 驱动程序可以在 error.go 中使用。还有一个由 VividCortex 维护的 MySQL 错误编号 的外部包。使用这样的列表,可以更好地编写上面的代码,如下所示:

if driverErr, ok := err.(*mysql.MySQLError); ok  {
    if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR {
        // 处理被拒绝的错误
    }
}

处理连接错误

如果您与数据库的连接被删除、终止或出现错误怎么办?

发生这种情况时,您不需要实施任何逻辑来重试失败的语句。作为 database/sql连接池 的一部分,内置了对失败连接的处理。如果您执行查询或其他语句,而底层连接失败,那么 Go 将重新打开一个新连接 (或仅从连接池获取另一个连接),然后重试,最多重试 10 次。

然而,这可能会有一些意想不到的后果。当其他错误情况发生时,可能会重试某些类型的错误。这也可能是特定于驱动程序的。MySQL 驱动程序发生的一个示例是,使用 kill 取消不需要的语句 (如长时间运行的查询) 会导致该语句最多重试 10 次。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
上一篇 下一篇
Summer
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~