如何在Go中为错误添加额外信息

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech Fund 来接受捐赠。

介绍

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

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

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

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

先决条件

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

  • Go version 1.13 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
  • (可选)阅读 Handling Errors in Go 可能有助于在本教程中更深入地解释错误处理,但本教程还将涵盖更高级别的一些相同主题。
  • (可选)本教程扩展了 Creating Custom Errors in Go 教程,并在原始教程中添加了 Go 的功能。 阅读前面的教程很有帮助,但不是严格要求的。

Go 中的返回和处理错误

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

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

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

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

mkdir projects
cd projects

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

mkdir errtutorial

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

cd errtutorial

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

go mod init errtutorial

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

nano main.go

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

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

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

项目/错误教程/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 函数接受一个数字,然后根据它是否被确定为有效值返回一个 error。 在这个程序中,数字1无效,返回错误that's odd。 数字2无效,返回错误uh ohvalidateValue 函数使用 fmt.Errorf 函数生成要返回的 error 值。 fmt.Errorf 函数便于返回错误,因为它允许您使用类似于 fmt.Printffmt.Sprintf 的格式来格式化错误消息,而无需随后传递 [X201X ] 至 errors.New

main 函数中,for 循环将通过迭代从 13 的每个数字开始,并将值存储在 [ X151X] 变量。 在循环体内,对 fmt.Printf 的调用将打印程序当前正在验证的数字。 然后,它将调用validateValue函数并传入num,当前正在验证的数字,并将错误结果存储在err变量中。 最后,如果 err 不是 nil,则表示验证期间发生错误,并且使用 fmt.Println 打印错误消息。 当没有遇到错误时,错误检查的 else 子句将打印 "valid!"

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

go run main.go

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

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

当您查看程序的输出时,您会看到程序试图验证所有三个数字。 它第一次说 validateValue 函数返回 that's odd 错误,这是 1 的值所期望的。 下一个值 2 也显示它返回了错误,但这次是 uh oh 错误。 最后,3 值返回 nil 作为错误值,表示没有错误并且数字有效。 validateValue 函数的编写方式,对于任何不是 12 的值,都将返回 nil 错误值。

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

使用 Sentinel 错误处理特定错误

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

检查特定类型错误的一种方法可能是在 error 类型上使用 Error 方法从错误中获取消息并将该值与您遇到的错误类型进行比较寻找。 想象一下,在您的程序中,当错误值为 uh oh 时,您希望显示除 there was an error: uh oh 以外的消息。 处理这种情况的一种方法是检查从 Error 方法返回的值,如下所示:

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

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

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

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

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

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 的值,因为它想让调用者知道发生了“哦哦”错误。 然后,错误处理代码将从 giveMeError 返回的 err 值与 errUhOh 进行比较,以查看是否发生了“呃哦”错误。 即使找到并修复了错字,所有代码仍然可以工作,因为错误检查正在检查 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 或 [ X206X]。 由于哨兵错误是值不变的基本错误,因此通常使用 errors.New 来创建它们。

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

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

项目/错误教程/main.go

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 值的一般错误输出,但是当它看到从 [X173X 返回的 errUhOh 错误时,它使用自定义的 oh no! 消息] 对于 2

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

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

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

包装和展开错误

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

在 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 动词包含在格式字符串中并且为该值提供了 error 时,从 fmt.Errorf 返回的错误将包括 error 的值] 包裹在正在创建的错误中。

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

项目/错误教程/main.go

...

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

输出将类似于以下内容:

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

在这个输出中有几件事需要注意。 首先,您将看到正在为值 1 打印的错误消息现在在错误消息中包含 run error: that's odd。 这表明错误被 runValidationfmt.Errorf 包装,并且被包装的错误的值 that's odd 包含在错误消息中。

但是,接下来有一个问题。 为 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 任何此外,它等于 wrapping errUhOh 的错误。 要修复 errUhOh 错误检查,您需要使用 errors.Unwrap 函数从包装器内部检索错误。

errors.Unwrap 的展开错误

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

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

项目/错误教程/main.go

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

输出将类似于以下内容:

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

现在,在输出中,您将看到对 2 输入值的 oh no! 错误处理又回来了。 添加到 if 语句的附加 errors.Unwrap 函数调用允许它在 err 本身是 errUhOh 值时检测 errUhOh就好像 err 是直接包装 errUhOh 的错误。

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

自定义包装错误

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

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

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

项目/错误教程/main.go

...

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

输出将类似于以下内容:

Outputvalidating 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!

您会看到输出现在显示错误被 value error: 包裹在 ValueError 中,在输出之前。 但是,uh oh 错误检测再次被破坏,因为 errUhOh 现在位于两层包装器中,ValueError 和 [X156X 中的 fmt.Errorf 包装器]。 代码代码仅在错误时使用 errors.Unwrap 一次,因此这导致第一个 errors.Unwrap(err) 现在只返回 *ValueError 而不是 errUhOh

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

项目/错误教程/main.go

...

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

输出将类似于以下内容:

Outputvalidating 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 特殊错误处理仍然不起作用。 我们希望看到特殊错误处理 oh no! 输出的验证 2 输入的行仍然显示默认的 there was an error: run error: ... 错误输出。 发生这种情况是因为 errors.Unwrap 函数不知道如何解开 ValueError 自定义错误类型。 为了展开自定义错误,它需要有自己的 Unwrap 方法,该方法将内部错误作为 error 值返回。 之前使用带有 %w 动词的 fmt.Errorf 创建错误时,Go 实际上是在为您创建一个已经添加了 Unwrap 方法的错误,因此您不需要这样做它自己。 不过,既然您正在使用自己的自定义函数,则需要添加自己的函数。

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

项目/错误教程/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

输出将类似于以下内容:

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

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

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

但是,错误处理变得有点笨拙且难以维护。 每次出现新的包装层时,您都必须在错误检查中添加另一个 errors.Unwrap 来处理它。 值得庆幸的是,errors 包中的 errors.Iserrors.As 函数可用于更轻松地处理包装错误。

处理包装错误

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

使用 errors.Is 检查错误值

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

要清理 errUhOh 错误处理,请打开 main.go 文件并在 main 函数中更新 errUhOh 检查以改用 errors.Is

项目/错误教程/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

输出将类似于以下内容:

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

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

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

使用 errors.As 检索错误类型

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

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

项目/错误教程/main.go

...

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。 通过获取对错误的引用,您可以查看错误是由于无法连接造成的,还是由于能够连接但未找到您的资源而导致的错误。 一旦你知道了这一点,你就可以用不同的方式处理错误。

要查看 errors.As 的运行情况,请保存文件并使用 go run 运行程序:

go run main.go

输出将类似于以下内容:

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

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

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

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

结论

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

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

如果您想了解有关如何使用新的错误功能的更多详细信息,Go 博客中有一篇关于 Working with Errors in Go 1.13 的文章。 errors 包 包的文档还包含更多信息。

本教程也是 DigitalOcean How to Code in Go 系列的一部分。 该系列涵盖了许多 Go 主题,从第一次安装 Go 到如何使用语言本身。