Skip to content

Latest commit

 

History

History
66 lines (45 loc) · 3.34 KB

7-4-Comparing-an-error-value-inaccurately.md

File metadata and controls

66 lines (45 loc) · 3.34 KB

7.4 错误值比较的坑

本节类似于我们刚刚讨论的那一节,但带有标记错误(错误值)。首先,我们将定义标记错误传达的信息。然后,我们将看到如何将错误与值进行比较。

标记错误是定义为全局变量的错误:

import "errors"
 
var ErrFoo = errors.New("foo")

通常,约定是以 Err 开头,后跟错误类型;这里 ErrFoo,标记错误传达了 预期 的错误。但是我们所说的预期错误是什么意思呢?让我们在 SQL 库的上下文中讨论它。

我们想设计一个 Query 方法,允许对数据库执行查询。此方法返回行的一部分。找不到行的情况下应该怎么处理呢?这里我们有两个选择:

  • 返回标记值;例如,一个 nil 切片(想想 strings.Index,如果子字符串不存在则返回标记值 -1)
  • 或者返回客户端可以检查的特定错误

让我们采用第二种方法。因此,如果未找到任何行,我们的方法可以返回特定错误。我们可以将此错误归类为预期错误,因为允许传递不返回任何行的请求。相反,如果出现网络问题或连接轮询错误等错误,我们将面临意外错误。这并不意味着我们不想处理意外错误;这意味着在语义上,它们传达了不同的含义。

如果我们看一下标准库,我们会发现很多标记错误的例子:

  • sql.ErrNoRows:当查询没有返回任何行时返回(这正是我们的情况)
  • io.EOF:当没有更多合法的输入时,返回 io.Reader

这就是标记错误背后的一般原则。它们传达了客户期望检查的预期错误。因此,作为一般准则:

  • 预期错误应设计为错误值(标记错误):var ErrFoo = errors.New("foo")
  • 意外错误应设计为错误类型:type BarError struct{ ... }BarError 实现 error 接口。

让我们回到常见的错误。我们如何将错误与特定值进行比较?我们可以使用 == 运算符:

err := query()
if err != nil {
        if err == sql.ErrNoRows {
                // ...
        } else {
                // ...
        }
}

在这里,我们调用了一个 query 函数并得到了一个错误。检查错误是否为 sql.ErrNoRows 是使用 == 运算符完成的。

但是,与我们在上一节中讨论的方式相同,也可以包装标记错误。 如果使用 fmt.Errorf%w 指令包装 sql.ErrNoRows,则 err == sql.ErrNoRows 将始终为 false。

同样,从 1.13 版开始的 Go 提供了答案。我们已经看到了 errors.As 是如何被用来检查一个类型的错误的。对于错误值,我们应该使用它的对应方法:errors.Is。 让我们重写前面的例子:

err := query()
if err != nil {
    if errors.Is(err, sql.ErrNoRows) {
        // ...
    } else {
        // ...
    }
}

即使错误是使用 %w 包装的,也要使用 errors.Is 来替代 == 运算符去做比较工作。

总之,如果我们在应用程序中使用 %w 指令和 fmt.Errorf 进行错误包装,则不应该使用 == 而是使用 errors.Is 来检查特定值的错误。因此,即使标记错误被包装,errors.Is 也能够递归地解包并将链中的每个错误与提供的值进行比较。

现在,是时候讨论错误处理最重要的方面之一了:不要两次处理错误。