_作者选择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 oh
。validateValue
函数使用fmt.Errorf
函数生成返回的error
值。fmt.Errorf
函数便于返回错误,因为它允许您使用类似于fmt.Printf
或fmt.Sprint tf
的格式设置错误消息的格式,而无需将该字符串
传递给errors.New
。
在main
函数中,for
循环首先迭代从1
到3
的每个数字,并将值存储在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函数的方式中,对于任何不是
1或
2的值,都将返回
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
。
在这种情况下,您可能希望以不同于其他错误的方式处理特定错误,通常会创建一个变量,其目的是保存错误值。这样,代码就可以检查该变量而不是字符串。通常,这些变量在名称中以err
或Err
开头,以表示它们是错误的。如果这个错误只在定义它的包中使用,你应该使用'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.Printf
或fmt.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!
当它看到2
的validateValue
返回的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‘S
fmt.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
现在位于两层包装器中,ValueError
和runValidation
中的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
添加一个返回Err
的Unwrap
方法,内部包装错误存储在该字段中:
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.Is
和errors.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.Is
和errors.As
。
使用这些新的错误函数可以更容易地包含有关您创建或处理的错误的更深层次的信息。它还会在将来验证您的代码,以确保即使在错误变得非常嵌套的情况下,您的错误检查仍能继续工作。
如果你想了解更多关于如何使用新的错误特性的细节,Go博客上有一篇关于[在Go 1.13中处理错误]的文章(https://go.dev/blog/go1.13-errors)。errors
包包的文档也包含更多信息。
本教程也是DigitalOcean如何在Go中编码]系列的一部分。该系列涵盖了许多Go主题,从第一次安装Go到如何使用语言本身。