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

_作者选择DISTICATION in Tech Fund]接受捐赠,作为WRITE FOR DONSITIONS计划)的一部分。

简介

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

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

此外,函数errors.Is和[errors.As](https://pkg.go.dev/errors As))可以更容易地确定特定错误是否被包装在给定错误中的任何位置,还可以让您直接访问该特定错误,而不需要自己解包所有错误。

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

前提条件

要学习本教程,您需要:

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

围棋返回式错误处理

当程序中出现错误时,最好处理这些错误,这样用户就看不到它们了--但要处理这些错误,您需要首先了解它们。在Go中,您可以通过使用特殊的interface类型,即error接口,从您的函数返回有关错误的信息来处理程序中的错误。只要任何GO类型定义了Error()字符串方法,使用error接口就可以将该GO类型作为error值返回。Go标准库提供了为这些返回值创建Error‘的功能,例如[fmt.Errorf`](https://pkg.go.dev/fmt#Errorf)函数。

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

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

首先,创建jects目录并导航到该目录:

1mkdir projects
2cd projects

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

1mkdir errtutorial

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

1cd errtutorial

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

1go mod init errtutorial

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

1nano main.go

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

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

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

 1[label projects/errtutorial/main.go]
 2package main
 3
 4import (
 5    "fmt"
 6)
 7
 8func validateValue(number int) error {
 9    if number == 1 {
10    	return fmt.Errorf("that's odd")
11    } else if number == 2 {
12    	return fmt.Errorf("uh oh")
13    }
14    return nil
15}
16
17func main() {
18    for num := 1; num <= 3; num++ {
19    	fmt.Printf("validating %d... ", num)
20    	err := validateValue(num)
21    	if err != nil {
22    		fmt.Println("there was an error:", err)
23    	} else {
24    		fmt.Println("valid!")
25    	}
26    }
27}

程序中的valiateValue函数接受一个数字,然后根据它是否被确定为有效值返回一个error。在本程序中,数字1无效,并返回错误That‘s odd。数字2无效,返回错误eh ohvalidateValue函数使用fmt.Errorf函数生成返回的error值。fmt.Errorf函数便于返回错误,因为它允许您使用类似于fmt.Printffmt.Sprint tf的格式设置错误消息的格式,而无需将该字符串传递给errors.New

main函数中,for循环首先迭代从13的每个数字,并将值存储在num变量中。在循环体内部,对fmt.Printf的调用将打印程序当前正在验证的数字。然后,它会调用valiateValue函数,传入当前要验证的数字num,并将错误结果存储在err变量中。最后,如果err不是nil,则表示验证过程中出错,并使用fmt.Println打印错误消息。当没有遇到错误时,错误检查的thers子句将打印Valid!

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

1go run main.go

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

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

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

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

使用Sentinel错误处理特定错误

当您从函数收到error值时,最基本的错误处理是检查error值是否为nil。这将告诉您函数是否有错误,但有时您可能希望为特定的错误情况自定义错误处理。例如,假设您有连接到远程服务器的代码,而您得到的唯一错误信息是You Have an Error。您可能希望知道错误是因为服务器不可用还是您的连接凭据无效。如果您知道该错误意味着用户的凭据是错误的,那么您可能希望立即让用户知道。但是,如果该错误意味着服务器不可用,则在让用户知道之前,您可能需要尝试重新连接几次。确定这些错误之间的差异使您可以编写更健壮和用户友好的程序。

检查特定类型的错误的一种方法可能是对error‘类型使用Error’方法来从错误中获取消息,并将该值与您要查找的错误类型进行比较。想象一下,在您的程序中,当错误值为eh oh时,您希望显示一条消息,而不是There Are Error:Uh oh。处理这种情况的一种方法是检查从`Error‘方法返回的值,如下所示:

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

像上面的代码一样,检查err.Error()的字符串值以确定它是否是值eh oh,在这种情况下可以工作。但是,如果程序中其他地方的呃oh错误字符串略有不同,代码就不会工作。如果错误消息本身需要更新,则以这种方式检查错误还会导致代码的重大更新,因为检查错误的每个位置都需要更新。以下面的代码为例:

1func giveMeError() error {
2    return fmt.Errorf("uh h")
3}
4
5err := giveMeError()
6if err.Error() == "uh h" {
7    // "uh h" error code
8}

在这段代码中,错误消息包含一个拼写错误,并且在eh oh中缺少‘o’。如果在某个时刻注意到并修复了这一点,但仅在几个位置添加了此错误检查之后,所有这些位置将需要将其检查更新为err.Error()==eh oh``。如果遗漏了一个错误,这可能很容易,因为这只是一个单一的字符更改,预期的自定义错误处理程序将不会运行,因为它需要eh h而不是eh oh

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

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

 1var errUhOh = fmt.Errorf("uh h")
 2
 3func giveMeError() error {
 4    return errUhOh
 5}
 6
 7err := giveMeError()
 8if err == errUhOh {
 9    // "uh oh" error code
10}

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

以这种方式检查和比较的误差值称为前置错误。定点错误是一种错误,它被设计为唯一的值,总是可以针对特定的含义进行比较。上面的errUhOh值将始终具有相同的含义,即发生了oh oh错误,因此程序可以通过将错误与errUhOh进行比较来确定是否发生了该错误。

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

一般情况下,在创建定点错误值时,会使用errors.New函数,来自errors包),而不是您目前使用的fmt.Errorf函数。不过,使用errors.New而不是fmt.Errorf并不会对错误的工作方式进行任何基本更改,而且这两个函数在大多数情况下都可以互换使用。这两个函数最大的区别是errors.New函数只会对静态消息产生错误,而fmt.Errorf函数允许用值格式化字符串,类似于fmt.Printffmt.Sprint tf。由于哨兵错误是基本错误,值不变,所以通常使用errors.New创建哨兵错误。

现在,将您的程序更新为使用eh oh错误的定点错误,而不是fmt.Errorf

首先,打开main.go文件,添加新的errUhOh发送错误,并更新程序以使用它。validateValue函数更新为返回前哨错误,而不是使用fmt.Errorf。对main函数进行更新,以检查errUhOh前哨错误,并在遇到该错误时打印‘oh no!’,而不是显示用于其他错误的‘There are an Error:’消息。

 1[label projects/errtutorial/main.go]
 2package main
 3
 4import (
 5    "errors"
 6    "fmt"
 7)
 8
 9var (
10    errUhOh = errors.New("uh oh")
11)
12
13func validateValue(number int) error {
14    if number == 1 {
15    	return fmt.Errorf("that's odd")
16    } else if number == 2 {
17    	return errUhOh
18    }
19    return nil
20}
21
22func main() {
23    for num := 1; num <= 3; num++ {
24    	fmt.Printf("validating %d... ", num)
25    	err := validateValue(num)
26    	if err == errUhOh {
27    		fmt.Println("oh no!")
28    	} else if err != nil {
29    		fmt.Println("there was an error:", err)
30    	} else {
31    		fmt.Println("valid!")
32    	}
33    }
34}

现在,保存您的代码并使用`Go Run‘再次运行您的程序:

1go run main.go

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

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

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

在本部分中,您创建了一个Go程序,该程序使用errors.New表示发生特定类型的错误时出现前哨错误。然而,随着时间的推移,随着程序的增长,您可能会希望在错误中包含更多信息,而不仅仅是错误值。该错误值不会给出任何关于错误发生位置或发生原因的上下文,而且在较大的程序中可能很难跟踪错误的具体信息。为了帮助进行故障排除并缩短调试时间,您可以利用错误包装来包含所需的细节。

包装和解包错误

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

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

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函数,以便不是直接调用valiateValue,而是调用runValidation

 1[label projects/errtutorial/main.go]
 2
 3...
 4
 5var (
 6    errUhOh = errors.New("uh oh")
 7)
 8
 9func runValidation(number int) error {
10    err := validateValue(number)
11    if err != nil {
12    	return fmt.Errorf("run error: %w", err)
13    }
14    return nil
15}
16
17...
18
19func main() {
20    for num := 1; num <= 3; num++ {
21    	fmt.Printf("validating %d... ", num)
22    	err := runValidation(num)
23    	if err == errUhOh {
24    		fmt.Println("oh no!")
25    	} else if err != nil {
26    		fmt.Println("there was an error:", err)
27    	} else {
28    		fmt.Println("valid!")
29    	}
30    }
31}

保存更新后,使用`Go Run‘运行更新后的程序:

1go run main.go

输出将如下所示:

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

在此输出中有几件事需要查看。首先,您将看到为值1‘打印的错误消息现在在错误消息中包含run error:That’s odd。这表明错误是由runValidation‘Sfmt.Errorf包装的,并且错误消息中包含了正在包装的错误的值That’s odd`。

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

使用errors.Unwrap解包错误

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

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

 1[label projects/errtutorial/main.go]
 2func main() {
 3    for num := 1; num <= 3; num++ {
 4    	fmt.Printf("validating %d... ", num)
 5    	err := runValidation(num)
 6    	if err == errUhOh || errors.Unwrap(err) == errUhOh {
 7    		fmt.Println("oh no!")
 8    	} else if err != nil {
 9    		fmt.Println("there was an error:", err)
10    	} else {
11    		fmt.Println("valid!")
12    	}
13    }
14}

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

1go run main.go

输出将如下所示:

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

现在,在输出中,您将看到2‘输入值的oh no!’错误处理又回来了。当err本身是errUhOh值时,以及err是直接包装errUhOh的错误时,您在if语句中添加的额外的errors.Unwap函数调用允许它检测errUhOh

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

自定义包装错误

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

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

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

 1[label projects/errtutorial/main.go]
 2
 3...
 4
 5var (
 6    errUhOh = fmt.Errorf("uh oh")
 7)
 8
 9type ValueError struct {
10    Value int
11    Err error
12}
13
14func newValueError(value int, err error) *ValueError {
15    return &ValueError{
16    	Value: value,
17    	Err:   err,
18    }
19}
20
21func (ve *ValueError) Error() string {
22    return fmt.Sprintf("value error: %s", ve.Err)
23}
24
25...
26
27func validateValue(number int) error {
28    if number == 1 {
29    	return newValueError(number, fmt.Errorf("that's odd"))
30    } else if number == 2 {
31    	return newValueError(number, errUhOh)
32    }
33    return nil
34}
35
36...

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

1go run main.go

输出将如下所示:

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

您将看到现在的输出显示,错误由输出中它们前面的Value Error:包装在ValueError中。但是,由于errUhOh现在位于两层包装器中,ValueErrorrunValidation中的fmt.Errorf包装器再次中断了eh oh错误检测。代码在出错时只使用了一次errors.Unwrap,所以这会导致第一个errors.Unprint(Err)现在只返回* ValueError,而不是errUhOh

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

 1[label projects/errtutorial/main.go]
 2
 3...
 4
 5func main() {
 6    for num := 1; num <= 3; num++ {
 7    	fmt.Printf("validating %d... ", num)
 8    	err := runValidation(num)
 9    	if err == errUhOh ||
10    		errors.Unwrap(err) == errUhOh ||
11    		errors.Unwrap(errors.Unwrap(err)) == errUhOh {
12    		fmt.Println("oh no!")
13    	} else if err != nil {
14    		fmt.Println("there was an error:", err)
15    	} else {
16    		fmt.Println("valid!")
17    	}
18    }
19}

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

1go run main.go

输出将如下所示:

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

您将看到,哦,errUhOh‘特殊错误处理仍然不起作用。在我们期望看到特殊错误处理oh no!‘输出的地方,验证2’输入的行仍然显示默认的‘There an Error:Run Error:...’错误输出。这是因为errors.Unwap函数不知道如何解包ValueError自定义错误类型。为了使自定义错误被解包,它需要有自己的Unwrap方法,该方法将内部错误作为error值返回。之前使用带有%w动词的fmt.Errorf创建错误时,Go实际上是在为您创建一个已经添加了Unwap`方法的错误,所以您不需要自己创建错误。既然您正在使用自己的定制函数,那么您需要添加自己的函数。

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

 1[label projects/errtutorial/main.go]
 2
 3...
 4
 5func (ve *ValueError) Error() string {
 6    return fmt.Sprintf("value error: %s", ve.Err)
 7}
 8
 9func (ve *ValueError) Unwrap() error {
10    return ve.Err
11}
12
13...

然后,保存新的`Unwap‘方法后,运行您的程序:

1go run main.go

输出将如下所示:

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

输出显示对errUhOh错误的oh no!错误处理再次工作,因为errors.Unwrap现在也可以展开ValueError

在本节中,您创建了一个新的自定义ValueError‘错误,以向您自己或您的用户提供有关验证过程的信息,作为错误消息的一部分。您还为您的ValueError增加了错误解包的支持,所以可以使用errors.Unwap`来访问被包装的错误。

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

处理包装错误

如果你需要为你的程序的每一层潜在的错误包装添加一个新的‘errors.Unwap’函数调用,那么它将变得非常长并且很难维护。出于这个原因,在GO 1.13版本中,errors包中还添加了两个额外的函数。这两个函数都允许您与错误交互,从而使处理错误变得更容易,而不管错误被包裹在其他错误中的深度有多深。errors.Is函数允许您检查特定的前置错误值是否位于包装错误中的任何位置。通过errors.As函数,您可以在包装的错误中的任何位置获取对特定类型错误的引用。

使用errors.Is检查错误值

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

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

 1[label projects/errtutorial/main.go]
 2
 3...
 4
 5func main() {
 6    for num := 1; num <= 3; num++ {
 7    	fmt.Printf("validating %d... ", num)
 8    	err := runValidation(num)
 9    	if errors.Is(err, errUhOh) {
10    		fmt.Println("oh no!")
11    	} else if err != nil {
12    		fmt.Println("there was an error:", err)
13    	} else {
14    		fmt.Println("valid!")
15    	}
16    }
17}

然后,保存您的代码并使用Go Run再次运行程序:

1go run main.go

输出将如下所示:

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

输出显示oh no!错误消息,这意味着即使只对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,函数返回true。如果没有匹配的错误类型,则返回False

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

 1[label projects/errtutorial/main.go]
 2
 3...
 4
 5func main() {
 6    for num := 1; num <= 3; num++ {
 7    	fmt.Printf("validating %d... ", num)
 8    	err := runValidation(num)
 9
10    	var valueErr *ValueError
11    	if errors.Is(err, errUhOh) {
12    		fmt.Println("oh no!")
13    	} else if errors.As(err, &valueErr) {
14    		fmt.Printf("value error (%d): %v\n", valueErr.Value, valueErr.Err)
15    	} else if err != nil {
16    		fmt.Println("there was an error:", err)
17    	} else {
18    		fmt.Println("valid!")
19    	}
20    }
21}

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

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

1go run main.go

输出将如下所示:

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

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

对于2值,输出还显示,即使errUhOh也包装在ValueError包装中,仍会执行oh no!特殊错误处理程序。这是因为在处理错误的if语句集合中,使用errors.Is处理errUhOh的特殊错误处理程序排在第一位。由于此处理程序在errors.As运行之前返回true,因此将执行特殊的oh no!处理程序。如果代码中的errors.As出现在errors.Is之前,则oh no!错误消息将变为与1值相同的Value Error...,但在本例中,它将打印Value Error(2):Uh oh

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

结论

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

使用这些新的错误函数可以更容易地包含有关您创建或处理的错误的更深层次的信息。它还会在将来验证您的代码,以确保即使在错误变得非常嵌套的情况下,您的错误检查仍能继续工作。

如果你想了解更多关于如何使用新的错误特性的细节,Go博客上有一篇关于[在Go 1.13中处理错误]的文章(https://go.dev/blog/go1.13-errors)。errors包的文档也包含更多信息。

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

Published At
Categories with 技术
Tagged with
comments powered by Disqus