跳转到主要内容

介绍


当 Go 中的函数失败时,该函数将使用错误接口返回一个值,以允许调用者处理该失败。在很多情况下,开发者会使用 fmt 包中的 fmt.Errorf 函数来返回这些值。但是,在 Go 1.13 之前,使用此函数的一个缺点是您会丢失有关可能导致返回错误的任何错误的信息。为了解决这个问题,开发人员要么使用包来提供一种将错误“包装”在其他错误中的方法,要么通过在他们的一种结构错误类型上实现 Error() 字符串方法来创建自定义错误。但是,如果您有许多不需要由调用者显式处理的错误,有时创建这些结构类型可能会很乏味,因此在 Go 1.13 中,该语言添加了一些功能以更容易处理这些情况。

一项功能是能够使用 fmt.Errorf 函数包装错误,该函数具有一个错误值,该错误值可以在以后解包以访问已包装的错误。这将错误包装功能构建到 Go 标准库中,因此不再需要使用第三方库。

此外,函数 errors.Is 和 errors.As 可以更容易地确定特定错误是否包含在给定错误中的任何位置,并且还可以让您直接访问该特定错误,而无需自己解开所有错误。

在本教程中,您将创建一个程序,该程序使用这些函数在从函数返回的错误中包含附加信息,然后创建您自己的支持包装和展开功能的自定义错误结构。

先决条件


要遵循本教程,您将需要:

  • 安装 1.13 或更高版本。要进行设置,请按照您的操作系统的如何安装 Go 教程进行操作。
  • (可选)阅读 Go 中的处理错误可能有助于本教程更深入地解释错误处理,但本教程还将在更高级别涵盖一些相同的主题。
  • (可选)本教程扩展了在 Go 中创建自定义错误教程,并在原始教程中添加了 Go 的功能。阅读前面的教程很有帮助,但不是严格要求的。


Go 中的返回和处理错误


当程序中发生错误时,处理这些错误是一种很好的做法,这样您的用户就不会看到它们——但要处理这些错误,您需要首先了解它们。在 Go 中,您可以通过使用特殊接口类型(错误接口)从函数返回有关错误的信息来处理程序中的错误。使用错误接口允许任何 Go 类型作为错误值返回,只要该类型定义了 Error() 字符串方法。 Go 标准库提供了为这些返回值创建错误的功能,例如 fmt.Errorf 函数。

在本节中,您将创建一个带有使用 fmt.Errorf 返回错误的函数的程序,您还将添加一个错误处理程序来检查该函数可能返回的错误。 (如果您想了解更多有关在 Go 中处理错误的信息,请参阅教程,在 Go 中处理错误。)

许多开发人员都有一个目录来保存当前项目。在本教程中,您将使用一个名为 projects 的目录。

首先,创建项目目录并导航到它:

mkdir projects
cd projects


从项目目录中,创建一个新的 errtutorial 目录以将新程序保存在:

mkdir errtutorial


接下来,使用 cd 命令导航到新目录:

cd errtutorial


进入 errtutorial 目录后,使用 go mod init 命令创建一个名为 errtutorial 的新模块:

go mod init errtutorial


创建 Go 模块后,使用 nano 或您喜欢的编辑器在 errtutorial 目录中打开一个名为 main.go 的文件:

nano main.go


接下来,您将编写一个程序。程序将遍历数字 1 到 3 并尝试使用名为 validateValue 的函数来确定这些数字是否有效。如果确定该数字无效,则程序将使用 fmt.Errorf 函数生成从该函数返回的错误值。 fmt.Errorf 函数允许您创建错误值,其中错误消息是您提供给函数的消息。它的工作方式与 fmt.Printf 类似,但不是将消息打印到屏幕上,而是将其作为错误返回。

然后,在 main 函数中,将检查错误值是否为 nil 值。如果是 nil 值,则函数成功且有效!消息被打印。如果不是,则打印收到的错误。

要开始您的程序,请将以下代码添加到您的 main.go 文件中:

package main

import (
    "fmt"
)

func validateValue(number int) error {
    if number == 1 {
        return fmt.Errorf("that's odd")
    } else if number == 2 {
        return fmt.Errorf("uh oh")
    }
    return nil
}

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := validateValue(num)
        if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

程序中的 validateValue 函数接受一个数字,然后根据它是否被确定为有效值返回一个错误。在这个程序中,数字 1 无效并返回奇数错误。数字 2 无效,返回错误 uh oh。 validateValue 函数使用 fmt.Errorf 函数来生成返回的错误值。 fmt.Errorf 函数便于返回错误,因为它允许您使用类似于 fmt.Printf 或 fmt.Sprintf 的格式来格式化错误消息,而无需将该字符串传递给 errors.New。

在 main 函数中,for 循环将首先遍历从 1 到 3 的每个数字,并将值存储在 num 变量中。在循环体内,对 fmt.Printf 的调用将打印程序当前正在验证的数字。然后,它将调用 validateValue 函数并传入当前正在验证的数字 num,并将错误结果存储在 err 变量中。最后,如果 err 不为零,则表示验证期间发生错误,并且使用 fmt.Println 打印错误消息。错误检查的 else 子句将打印“有效!”当没有遇到错误时。

保存更改后,使用 go run 命令运行程序,并将 main.go 作为 errtutorial 目录中的参数:

go run main.go


运行程序的输出将显示每个数字都运行了验证,并且数字 1 和数字 2 返回了相应的错误:

Output
validating 1... there was an error: that's odd
validating 2... there was an error: uh oh
validating 3... valid!


当您查看程序的输出时,您会看到程序试图验证所有三个数字。第一次它说 validateValue 函数返回了一个奇怪的错误,这是值 1 所期望的。下一个值 2 也表明它返回了一个错误,但这次是呃哦错误。最后,对于错误值,3 值返回 nil,这意味着没有错误并且数字是有效的。按照 validateValue 函数的编写方式,对于任何不是 1 或 2 的值,都将返回 nil 错误值。

在本节中,您使用 fmt.Errorf 创建从函数返回的错误值。您还添加了一个错误处理程序,以在函数返回任何错误时打印出错误消息。但有时,了解错误的含义可能很有用,而不仅仅是发生了错误。在下一节中,您将学习为特定情况自定义错误处理。

使用 Sentinel 错误处理特定错误


当你从函数接收到错误值时,最基本的错误处理是检查错误值是否为 nil。这将告诉您函数是否有错误,但有时您可能希望针对特定错误情况自定义错误处理。例如,假设您有代码连接到远程服务器,而您返回的唯一错误信息是“您遇到了错误”。您可能希望判断错误是因为服务器不可用还是您的连接凭据无效。如果您知道错误意味着用户的凭据错误,您可能希望立即让用户知道。但是,如果错误意味着服务器不可用,您可能需要尝试重新连接几次,然后再让用户知道。确定这些错误之间的差异可以让您编写更健壮和用户友好的程序。

检查特定类型错误的一种方法可能是对错误类型使用 Error 方法从错误中获取消息并将该值与您正在查找的错误类型进行比较。想象一下,在你的程序中,你想要显示一条消息,而不是出现错误:uh oh 当错误值为 uh oh。处理这种情况的一种方法是检查 Error 方法返回的值,如下所示:

if err.Error() == "uh oh" {
    // Handle 'uh oh' error.
    fmt.Println("oh no!")
}


在这种情况下,检查 err.Error() 的字符串值以查看它是否是值 uh oh,如上面的代码所示。但是如果 uh oh 错误字符串在程序的其他地方略有不同,则代码将无法工作。如果错误的消息本身需要更新,以这种方式检查错误也可能导致代码的重大更新,因为检查错误的每个地方都需要更新。以下面的代码为例:

func giveMeError() error {
    return fmt.Errorf("uh h")
}

err := giveMeError()
if err.Error() == "uh h" {
    // "uh h" error code
}

在此代码中,错误消息包含一个拼写错误,并且缺少 uh oh 中的 o。如果在某个时候注意到并修复了这一点,但只有在几个地方添加了这个错误检查之后,所有这些地方都需要将它们的检查更新为 err.Error() == "uh oh"。如果错过了一个,这可能很容易,因为它只是一个字符更改,预期的自定义错误处理程序将不会运行,因为它期待 uh h 而不是 uh oh。

在这种情况下,您可能希望以与其他方式不同的方式处理特定错误,通常会创建一个用于保存错误值的变量。这样,代码可以检查该变量而不是字符串。通常,这些变量的名称以 err 或 Err 开头,表示它们是错误的。如果该错误仅用于定义它的包中,则需要使用 err 前缀。如果该错误打算在其他地方使用,您将改为使用 Err 前缀使其成为导出值,类似于函数或结构。

现在,假设您在之前的错字示例中使用了这些错误值之一:

var errUhOh = fmt.Errorf("uh h")

func giveMeError() error {
    return errUhOh
}

err := giveMeError()
if err == errUhOh {
    // "uh oh" error code
}

在此示例中,变量 errUhOh 被定义为“uh oh”错误(即使拼写错误)的错误值。 giveMeError 函数返回 errUhOh 的值,因为它想让调用者知道发生了“uh oh”错误。然后,错误处理代码将 giveMeError 返回的 err 值与 errUhOh 进行比较,以查看是否发生了“uh oh”错误。即使找到并修复了拼写错误,所有代码仍然可以工作,因为错误检查正在检查 errUhOh 的值,而 errUhOh 的值是 giveMeError 返回的错误值的固定版本。

旨在以这种方式检查和比较的错误值称为标记错误。哨兵错误是一种设计为唯一值的错误,始终可以针对特定含义进行比较。上面的 errUhOh 值始终具有相同的含义,即发生了“uh oh”错误,因此程序可以依靠将错误与 errUhOh 进行比较来确定是否发生了该错误。

Go 标准库还定义了许多在开发 Go 程序时可用的标记错误。一个例子是 sql.ErrNoRows 错误。当数据库查询未返回任何结果时,将返回 sql.ErrNoRows 错误,因此可以与连接错误不同地处理该错误。由于它是一个标记错误,因此可以在错误检查代码中与它进行比较,以了解查询何时不返回任何行,并且程序可以以不同于其他错误的方式处理它。

通常,在创建标记错误值时,会使用 errors 包中的 errors.New 函数,而不是您目前使用的 fmt.Errorf 函数。但是,使用 errors.New 而不是 fmt.Errorf 不会对错误的工作方式进行任何基本更改,并且这两个函数在大多数情况下都可以互换使用。两者最大的区别是errors.New 函数只会使用静态消息创建错误,而fmt.Errorf 函数允许使用值格式化字符串,类似于fmt.Printf 或fmt.Sprintf。由于哨兵错误是值不变的基本错误,因此通常使用 errors.New 来创建它们。

现在,更新您的程序以使用“uh oh”错误而不是 fmt.Errorf 的标记错误。

首先,打开 main.go 文件添加新的 errUhOh 标记错误并更新程序以使用它。 validateValue 函数已更新为返回标记错误,而不是使用 fmt.Errorf。更新主函数以检查 errUhOh 哨兵错误并打印 oh no!当它遇到它而不是出现错误时:它显示其他错误的消息。

package main

import (
    "errors"
    "fmt"
)

var (
    errUhOh = errors.New("uh oh")
)

func validateValue(number int) error {
    if number == 1 {
        return fmt.Errorf("that's odd")
    } else if number == 2 {
        return errUhOh
    }
    return nil
}

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := validateValue(num)
        if err == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

现在,保存您的代码并使用 go run 再次运行您的程序:

go run main.go


这次输出将显示 1 值的一般错误输出,但它使用自定义哦不!当它看到从 validateValue 为 2 返回的 errUhOh 错误时的消息:

Output
validating 1... there was an error: that's odd
validating 2... oh no!
validating 3... valid!


在错误检查中使用标记错误可以更轻松地处理特殊错误情况。例如,它们可以帮助确定您正在阅读的文件是否因为到达文件末尾而失败,这由 io.EOF 哨兵错误表示,或者是否由于其他原因而失败。

在本节中,您创建了一个使用标记错误的 Go 程序,使用 errors.New 来表示何时发生特定类型的错误。但是,随着程序的增长,随着时间的推移,您可能会希望在错误中包含更多信息,而不仅仅是 uh oh error 值。这个错误值没有给出错误发生的位置或发生原因的任何上下文,并且很难在较大的程序中追踪错误的细节。为了帮助进行故障排除并缩短调试时间,您可以使用错误包装来包含您需要的细节。

包装和展开错误


包装错误意味着获取一个错误值并将另一个错误值放入其中,就像包装好的礼物一样。但是,与包装好的礼物类似,您需要打开它才能知道里面是什么。包装错误允许您包含有关错误来自何处或如何发生的附加信息,而不会丢失原始错误值,因为它位于包装器内部。

在 Go 1.13 之前,可以包装错误,因为您可以创建包含原始错误的自定义错误值。但是您要么必须创建自己的包装器,要么使用已经为您完成工作的库。不过,在 Go 1.13 中,Go 通过添加 errors.Unwrap 函数和 fmt.Errorf 函数的 %w 动词,在标准库中添加了对包装和解包错误的支持。在本节中,您将更新您的程序以使用 %w 动词来包装带有更多信息的错误,然后您将使用 errors.Unwrap 来检索包装的信息。

fmt.Errorf包装错误


包装和展开错误时要检查的第一个功能是对现有 fmt.Errorf 函数的补充。过去,fmt.Errorf 用于使用动词(例如用于字符串的 %s 和用于通用值的 %v)来创建带有附加信息的格式化错误消息。 Go 1.13 添加了一个带有特殊情况的新动词,即 %w 动词。当 %w 动词包含在格式字符串中并且为该值提供了错误时,从 fmt.Errorf 返回的错误将包含包含在正在创建的错误中的错误的值。

现在,打开 main.go 文件并更新它以包含一个名为 runValidation 的新函数。此函数将获取当前正在验证的号码并对该号码运行所需的任何验证。在这种情况下,它只需要运行 validateValue 函数。如果在验证值时遇到错误,它将使用 fmt.Errorf 和 %w 动词包装错误以显示发生了运行错误,然后返回该新错误。您还应该更新 main 函数,而不是直接调用 validateValue 而是调用 runValidation:

...

var (
    errUhOh = errors.New("uh oh")
)

func runValidation(number int) error {
    err := validateValue(number)
    if err != nil {
        return fmt.Errorf("run error: %w", err)
    }
    return nil
}

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if err == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

保存更新后,使用 go run 运行更新的程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... there was an error: run error: that's odd
validating 2... there was an error: run error: uh oh
validating 3... valid!


在这个输出中有几件事需要注意。首先,您将看到为值 1 打印的错误消息现在包括运行错误:错误消息中的这很奇怪。这表明错误被 runValidation 的 fmt.Errorf 包装,并且被包装的错误的值,这很奇怪,包含在错误消息中。

不过,接下来有一个问题。为 errUhOh 错误添加的特殊错误处理未运行。如果您查看验证 2 输入的行,您会看到它显示默认错误消息 there was an error: run error: uh oh 而不是预期的 oh no!信息。您知道 validateValue 函数仍然返回 uh oh 错误,因为您可以在包装错误的末尾看到它,但是 errUhOh 的错误检测不再起作用。发生这种情况是因为 runValidation 返回的错误不再是 errUhOh,而是由 fmt.Errorf 创建的包装错误。当 if 语句尝试将 err 变量与 errUhOh 进行比较时,它返回 false,因为 err 不再等于 errUhOh,它等于包装 errUhOh 的错误。要修复 errUhOh 错误检查,您需要使用 errors.Unwrap 函数从包装器内部检索错误。

用errors.Unwrap解包错误


除了 Go 1.13 中添加的 %w 动词之外,Go 错误包中还添加了一些新函数。其中之一,errors.Unwrap 函数,将错误作为参数,如果传入的错误是错误包装器,它将返回包装的错误。如果提供的错误不是包装器,则该函数将返回 nil。

现在,再次打开 main.go 文件,并使用 errors.Unwrap 更新 errUhOh 错误检查以处理 errUhOh 被包装在错误包装器中的情况:

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if err == errUhOh || errors.Unwrap(err) == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

保存编辑后,再次运行程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... there was an error: run error: that's odd
validating 2... oh no!
validating 3... valid!


现在,在输出中,你会看到哦,不! 2 输入值的错误处理又回来了。您添加到 if 语句的附加 errors.Unwrap 函数调用允许它检测 errUhOh,无论是当 err 本身是 errUhOh 值时,还是 err 是直接包装 errUhOh 的错误。

在本节中,您使用添加到 fmt.Errorf 的 %w 动词将 errUhOh 错误包装在另一个错误中并为其提供附加信息。然后,您使用 errors.Unwrap 来访问包装在另一个错误中的 errorUhOh 错误。将错误包含在其他错误中作为字符串值对于人类阅读错误消息是可以的,但有时您需要在错误包装器中包含其他信息以帮助程序处理错误,例如 HTTP 请求错误中的状态代码。发生这种情况时,您可以创建一个新的自定义错误以返回。

自定义包装错误


由于 Go 对错误接口的唯一规则是它包含一个 Error 方法,因此可以将许多 Go 类型转换为自定义错误。一种方法是定义一个带有有关错误的额外信息的结构类型,然后还包括一个错误方法。

对于验证错误,了解实际导致错误的值可能很有用。接下来,让我们创建一个新的 ValueError 结构体,其中包含导致错误的 Value 字段和包含实际验证错误的 Err 字段。自定义错误类型通常使用类型名称末尾的 Error 后缀来表示它是符合错误接口的类型。

打开你的 main.go 文件并添加新的 ValueError 错误结构,以及一个 newValueError 函数来创建错误的实例。您还需要为 ValueError 创建一个名为 Error 的方法,因此该结构将被视为错误。每当错误转换为字符串时,此错误方法应返回您希望显示的值。在这种情况下,它将使用 fmt.Sprintf 返回一个显示 value error: 的字符串,然后是包装的错误。此外,更新 validateValue 函数,使其不只返回基本错误,而是使用 newValueError 函数返回自定义错误:

...

var (
    errUhOh = fmt.Errorf("uh oh")
)

type ValueError struct {
    Value int
    Err   error
}

func newValueError(value int, err error) *ValueError {
    return &ValueError{
        Value: value,
        Err:   err,
    }
}

func (ve *ValueError) Error() string {
    return fmt.Sprintf("value error: %s", ve.Err)
}

...

func validateValue(number int) error {
    if number == 1 {
        return newValueError(number, fmt.Errorf("that's odd"))
    } else if number == 2 {
        return newValueError(number, errUhOh)
    }
    return nil
}

...

保存更新后,使用 go run 再次运行程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... there was an error: run error: value error: that's odd
validating 2... there was an error: run error: value error: uh oh
validating 3... valid!


你会看到输出现在显示错误被包裹在 ValueError 中,在输出中它们之前的值是 error:。 但是,uh oh 错误检测再次被破坏,因为 errUhOh 现在位于两层包装器内,ValueError 和来自 runValidation 的 fmt.Errorf 包装器。 代码代码只使用errors.Unwrap一次错误,所以这会导致第一个errors.Unwrap(err)现在只返回一个*ValueError而不是errUhOh。

解决此问题的一种方法是更新 errUhOh 检查以添加额外的错误检查,该检查调用 errors.Unwrap() 两次以解开两个层。 要添加此内容,请打开您的 main.go 文件并更新您的 main 函数以包含此更改:

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if err == errUhOh ||
            errors.Unwrap(err) == errUhOh ||
            errors.Unwrap(errors.Unwrap(err)) == errUhOh {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

现在,保存您的 main.go 文件并使用 go run 再次运行您的程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... there was an error: run error: value error: that's odd
validating 2... there was an error: run error: value error: uh oh
validating 3... valid!


你会看到,呃,errUhOh 特殊错误处理仍然不起作用。验证我们期望看到特殊错误处理的 2 输入的行哦,不!输出仍然显示默认有错误:运行错误:...错误输出。发生这种情况是因为 errors.Unwrap 函数不知道如何解开 ValueError 自定义错误类型。为了解包自定义错误,它需要有自己的 Unwrap 方法,该方法将内部错误作为错误值返回。之前使用带有 %w 动词的 fmt.Errorf 创建错误时,Go 实际上是在为您创建一个已经添加了 Unwrap 方法的错误,因此您不需要自己做。但是,既然您正在使用自己的自定义函数,则需要添加自己的函数。

要最终修复 errUhOh 错误情况,请打开 main.go 并向返回 Err 的 ValueError 添加一个 Unwrap 方法,内部包装错误存储在该字段中:

projects/errtutorial/main.go

...

func (ve *ValueError) Error() string {
    return fmt.Sprintf("value error: %s", ve.Err)
}

func (ve *ValueError) Unwrap() error {
    return ve.Err
}

...

然后,一旦你保存了新的 Unwrap 方法,运行你的程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... there was an error: run error: value error: that's odd
validating 2... oh no!
validating 3... valid!


输出显示哦,不! errUhOh 错误的错误处理再次起作用,因为 errors.Unwrap 现在也能够解包 ValueError。

在本节中,您创建了一个新的自定义 ValueError 错误,以向您自己或您的用户提供有关验证过程的信息作为错误消息的一部分。您还向 ValueError 添加了对错误解包的支持,因此 errors.Unwrap 可用于访问已包装的错误。

但是,错误处理变得有点笨拙且难以维护。每次出现新的包装层时,您都必须添加另一个错误。打开错误检查以处理它。值得庆幸的是,errors 包中的 errors.Is 和 errors.As 函数可以使处理包装的错误更容易。

处理包装错误


如果您需要添加一个新的errors.Unwrap 函数调用每个潜在的错误包装你的程序层,它会变得很长并且难以维护。出于这个原因,Go 1.13 版本的 errors 包中还添加了两个附加功能。这两个函数都允许您与错误进行交互,无论它们被包裹在其他错误中的程度如何,都可以更轻松地处理错误。 errors.Is 函数允许您检查特定标记错误值是否在包装错误内的任何位置。 errors.As 函数允许您在包装错误内的任何位置获取对某种类型错误的引用。

使用errors.Is检查错误值


使用 errors.Is 检查特定错误会使 errUhOh 特殊错误处理时间更短,因为它会处理您手动执行的所有嵌套错误展开。该函数有两个错误参数,第一个是您实际收到的错误,第二个参数是您要检查的错误。

要清理 errUhOh 错误处理,请打开您的 main.go 文件并更新主函数中的 errUhOh 检查以使用 errors.Is 代替:

projects/errtutorial/main.go

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)
        if errors.Is(err, errUhOh) {
            fmt.Println("oh no!")
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

然后,保存代码并使用 go run 再次运行程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... there was an error: run error: value error: that's odd
validating 2... oh no!
validating 3... valid!


输出显示哦,不!错误信息,这意味着即使对 errUhOh 只有一次错误检查,它仍然会在错误链中找到。 errors.Is 利用错误类型的 Unwrap 方法继续深入挖掘错误链,直到找到您要查找的错误值、标记错误或遇到返回 nil 值的 Unwrap 方法。

由于错误包装作为 Go 中的一项功能存在,因此推荐使用 errors.Is 来检查特定错误。它不仅可以用于您自己的错误值,还可以用于其他错误值,例如本教程前面提到的 sql.ErrNoRows 错误。

检索带有errors.As的错误类型


Go 1.13 中添加到 errors 包的最后一个函数是 errors.As 函数。当您想要获取对某种类型错误的引用以更详细地与之交互时,使用此函数。例如,您之前添加的 ValueError 自定义错误允许访问在错误的 Value 字段中验证的实际值,但只有在首先引用该错误时才能访问它。这就是errors.As进来的地方。你可以给errors.As一个错误,类似于errors.Is,以及一个错误类型的变量。然后它将通过错误链查看是否有任何包装错误与提供的类型匹配。如果匹配,则为错误类型传入的变量将设置为错误errors.As found,并且该函数将返回true。如果没有错误类型匹配,它将返回 false。

使用 errors.As 您现在可以利用 ValueError 类型在错误处理程序中显示其他错误信息。最后一次打开你的 main.go 文件并更新 main 函数,为 ValueError 类型的错误添加一个新的错误处理案例,打印出值错误、无效数字和验证错误:

...

func main() {
    for num := 1; num <= 3; num++ {
        fmt.Printf("validating %d... ", num)
        err := runValidation(num)

        var valueErr *ValueError
        if errors.Is(err, errUhOh) {
            fmt.Println("oh no!")
        } else if errors.As(err, &valueErr) {
            fmt.Printf("value error (%d): %v\n", valueErr.Value, valueErr.Err)
        } else if err != nil {
            fmt.Println("there was an error:", err)
        } else {
            fmt.Println("valid!")
        }
    }
}

在上面的代码中,您声明了一个新的 valueErr 变量并使用了 errors.As 来获取对 ValueError 的引用(如果它包含在 err 值中)。通过以 ValueError 的形式访问错误,您就可以访问该类型提供的任何其他字段,例如验证失败的实际值。如果验证逻辑发生在程序的更深处并且您通常无法访问这些值来向用户提示可能出现问题的地方,这可能会有所帮助。另一个可能有用的例子是,如果您正在做网络编程并遇到 net.DNSError。通过获取对错误的引用,您可以查看错误是由于无法连接造成的,还是由于能够连接但未找到您的资源而导致的错误。一旦你知道了这一点,你就可以用不同的方式处理错误。

要查看错误。就像实际操作一样,保存文件并使用 go run 运行程序:

go run main.go


输出将类似于以下内容:

Output
validating 1... value error (1): that's odd
validating 2... oh no!
validating 3... valid!


这次在输出中您不会看到默认的错误:...消息,因为所有错误都由其他错误处理程序处理。验证 1 的输出显示 errors.As 错误检查返回 true,因为值 error ... 正在显示错误消息。由于errors.As函数返回true,valueErr变量被设置为ValueError,可以通过访问valueErr.Value来打印验证失败的值。

对于 2 值,输出还显示即使 errUhOh 也包装在 ValueError 包装器中,哦不!特殊的错误处理程序仍在执行。这是因为使用errors.Is for errUhOh 的特殊错误处理程序首先出现在处理错误的if 语句集合中。由于此处理程序在错误之前返回 true。即使运行,特殊的哦不!处理程序是执行的。如果errors.As在你的代码之前出现在errors.Is,哦不!错误消息将变为与 1 值相同的值 error ...,除非在这种情况下它会打印 value error (2): uh oh。

在本节中,您更新了您的程序以使用 errors.Is 函数来删除许多对错误的额外调用。解包并使您的错误处理代码更加健壮和面向未来。您还使用了 errors.As 函数来检查是否有任何包装错误是 ValueError,如果找到,则使用值上的字段。

结论


在本教程中,您使用 %w 格式动词包裹了一个错误,并使用 errors.Unwrap 展开了一个错误。您还创建了一个支持错误的自定义错误类型。在您自己的代码中展开。最后,您使用自定义错误类型来探索新的辅助函数errors.Is 和errors.As。

使用这些新的错误函数可以更轻松地包含有关您创建或处理的错误的更深入信息。它还可以为您的代码提供未来证明,以确保即使错误变得深入嵌套,您的错误检查也能继续工作。

如果你想了解更多关于如何使用新的错误特性的细节,Go 博客有一篇关于在 Go 1.13 中处理错误的文章。错误包包的文档还包括更多信息。

文章链接