健壮的代码需要对用户输入错误、网络连接故障和磁盘故障等意外情况做出正确反应。_错误处理_是识别程序何时处于意外状态,并采取步骤记录诊断信息以供以后调试的过程。
与其他需要开发人员使用专门语法处理错误的语言不同,Go中的错误是从函数返回的error
类型的值,就像其他任何值一样。要处理围棋中的错误,我们必须检查这些函数可能返回的错误,确定是否发生了错误,并采取适当的操作来保护数据,并告诉用户或操作员发生了错误。
创建错误
在我们可以处理错误之前,我们需要先创建一些错误。标准库提供了两个内置的创建错误的函数:errors.New
和fmt.Errorf
。这两个函数都允许您指定稍后可以呈现给用户的自定义错误消息。
errors.New
只有一个参数--一个错误消息作为一个字符串,你可以定制它来提醒你的用户哪里出了问题。
尝试运行以下示例,以查看标准输出中显示的由errors.New
创建的错误:
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8func main() {
9 err := errors.New("barnacles")
10 fmt.Println("Sammy says:", err)
11}
1[secondary_label Output]
2Sammy says: barnacles
我们使用标准库中的errors.New
函数创建了一条新的错误消息,错误消息字符串为`)]的建议,对错误消息使用小写。
最后,我们使用fmt.Println
函数将错误消息与Sammy Said:
组合在一起。
fmt.Errorf
函数允许您动态构建错误消息。它的第一个参数是一个字符串,其中包含您的错误消息,并带有占位符的值,例如%s
表示字符串,%d
表示整数。fmt.Errorf
将此格式字符串后面的参数按顺序插入到这些占位符中:
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 err := fmt.Errorf("error occurred at: %v", time.Now())
10 fmt.Println("An error happened:", err)
11}
1[secondary_label Output]
2An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103
我们使用fmt.Errorf
函数来构建包含当前时间的错误消息。我们提供给fmt.Errorf
的格式字符串包含%v
格式指令,该指令告诉fmt.Errorf
对格式字符串后提供的第一个参数使用默认格式。该参数将是当前时间,由标准库中的time.Now
函数提供。与前面的示例类似,我们将错误消息与短前缀组合在一起,并使用fmt.Println
函数将结果打印到标准输出。
错误处理
通常,您不会看到像这样创建的错误立即用于其他目的,如前面的示例所示。在实践中,更常见的方式是创建一个错误,然后在出现错误时从函数中返回该错误。然后,该函数的调用者将使用if
语句来查看是否存在错误或nil
-一个未初始化的值。
下一个例子包括一个总是返回错误的函数。请注意,当您运行程序时,它会产生与上一个示例相同的输出,即使函数这次返回错误。在其他位置声明错误不会更改错误消息。
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8func boom() error {
9 return errors.New("barnacles")
10}
11
12func main() {
13 err := boom()
14
15 if err != nil {
16 fmt.Println("An error occurred:", err)
17 return
18 }
19 fmt.Println("Anchors away!")
20}
1[secondary_label Output]
2An error occurred: barnacles
在这里,我们定义了一个名为om()
的函数,该函数返回一个使用errors.New
构造的error
。然后,我们调用该函数并使用err:=boom()
行捕获错误。
一旦我们分配了这个错误,我们就会检查它是否带有if err!=nil
条件。在这里,条件的计算结果总是为true
,因为我们总是从om()
返回一个error
。
情况并不总是这样,所以最好使用逻辑来处理不存在错误的情况(nil
)和存在错误的情况。当出现错误时,我们使用fmt.Println
打印错误以及前缀,就像我们在前面的示例中所做的那样。最后,我们使用reur
语句跳过fmt.Println()
的执行,因为这应该只在没有发生错误时执行。
<$>[备注]
注意: 上例中的if err!=nil
结构是围棋编程语言中错误处理的主力。在函数可能产生错误的地方,使用if
语句检查是否发生错误是很重要的。这样,惯用的围棋代码在第一个缩进级别自然就有了它的[)逻辑,而在第二个缩进级别就有了所有的
悲伤路径`逻辑。
<$>
IF语句有一个可选的赋值子句,可用于帮助压缩函数调用和处理其错误。
运行下一个程序以查看与前面示例相同的输出,但这一次使用复合if
语句来减少一些样板:
1package main
2
3import (
4 "errors"
5 "fmt"
6)
7
8func boom() error {
9 return errors.New("barnacles")
10}
11
12func main() {
13 if err := boom(); err != nil {
14 fmt.Println("An error occurred:", err)
15 return
16 }
17 fmt.Println("Anchors away!")
18}
1[secondary_label Output]
2An error occurred: barnacles
和前面一样,我们有一个函数om()
,它总是返回错误。我们将om()
返回的错误赋给err
,作为if
语句的第一部分。在if
语句的第二部分中,分号后面的那个err
变量随后可用。我们检查错误是否存在,并像前面那样用短前缀字符串打印错误。
在本节中,我们学习了如何处理只返回错误的函数。这些函数很常见,但能够处理可能返回多个值的函数的错误也很重要。
随值返回错误
返回单个错误值的函数通常是那些影响某些状态更改的函数,例如向数据库插入行。同样常见的是,如果函数成功完成,则返回一个值,如果函数失败,则返回一个潜在错误。Go语言允许函数返回多个结果,可以同时返回值和错误类型。
为了创建一个返回多个值的函数,我们在函数签名的括号内列出每个返回值的类型。例如,返回一个string
和一个error
的capitalize
函数将使用func capitalize(name string)(string,error){}
声明。(string,error)
部分告诉Go编译器这个函数将按顺序返回一个string
和一个error
。
运行以下程序,查看同时返回字符串
和错误
的函数的输出:
1package main
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9func capitalize(name string) (string, error) {
10 if name == "" {
11 return "", errors.New("no name provided")
12 }
13 return strings.ToTitle(name), nil
14}
15
16func main() {
17 name, err := capitalize("sammy")
18 if err != nil {
19 fmt.Println("Could not capitalize:", err)
20 return
21 }
22
23 fmt.Println("Capitalized name:", name)
24}
1[secondary_label Output]
2Capitalized name: SAMMY
我们将Capital()
定义为一个函数,该函数接受一个字符串(要大写的名称),并返回一个字符串和一个错误值。在main()
中,我们调用Capital()
,并将函数返回的两个值赋给name
和err
变量,方法是在:=
运算符的左侧用逗号分隔它们。在此之后,我们像前面的示例一样执行if err!=nil
检查,如果出现错误,则使用fmt.Println
将错误输出到标准输出。如果没有错误,我们会打印大写名称:SAMMY
。
尝试将name中的字符串
),则会收到错误
Ccan‘t Capitalize:no name Provided`。
当capalize
函数的调用者为name
参数提供空字符串时,函数将返回错误。当name
参数不是空字符串时,Capitalize()
会使用strass.ToTitle
将name
参数大写,并返回nil
作为错误值。
本例遵循一些微妙的约定,这是Go代码的典型约定,但Go编译器并未强制执行。当一个函数返回多个值时,包括一个错误,约定要求我们返回error
作为最后一项。当从一个有多个返回值的函数返回一个error
时,惯用的Go代码也会将每个非错误值设置为a_zer值_。例如,零值是用于字符串的空字符串,用于整数的0‘,用于结构类型的空结构,以及用于接口和指针类型的
nil`等等。我们在关于变量和constants.的教程》中更详细地介绍了零值
减少样板文件
在函数需要返回许多值的情况下,遵守这些约定可能会变得单调乏味。我们可以使用匿名function来帮助减少样板。匿名函数是分配给变量的过程。与我们在前面的示例中定义的函数不同,它们只在声明它们的函数中可用-这使得它们非常适合作为可重用的助手逻辑的简短片段。
下面的程序修改了最后一个示例,以包括我们要大写的名称的长度。由于它有三个要返回的值,如果没有匿名函数来帮助我们,处理错误可能会变得很麻烦:
1package main
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9func capitalize(name string) (string, int, error) {
10 handle := func(err error) (string, int, error) {
11 return "", 0, err
12 }
13
14 if name == "" {
15 return handle(errors.New("no name provided"))
16 }
17
18 return strings.ToTitle(name), len(name), nil
19}
20
21func main() {
22 name, size, err := capitalize("sammy")
23 if err != nil {
24 fmt.Println("An error occurred:", err)
25 }
26
27 fmt.Printf("Capitalized name: %s, length: %d", name, size)
28}
1[secondary_label Output]
2Capitalized name: SAMMY, length: 5
在main()
中,我们现在捕获从capalize
返回的三个参数,分别为name
、size
和err
。然后,我们通过检查err
变量是否不等于nil
来检查capalize
是否返回了error
。在尝试使用Capitalize
返回的任何其他值之前,这一点很重要,因为匿名函数handle
可以将这些值设置为零值。因为我们提供了字符串sammy
,所以没有发生错误,所以我们打印出大写的名称及其长度。
您可以再次尝试将)
,以查看打印的错误案例(发生错误:未提供名称
)。
在Capitalize
中,我们将handle
变量定义为匿名函数。它接受单个错误,并以与Capitalize
的返回值相同的顺序返回相同的值。handle
将这些值设置为零值,并将作为其参数传递的error
作为最终返回值转发。使用它,我们可以通过在以error
为参数的handle
调用前面使用reur
语句来返回Capitalize
中遇到的任何错误。
请记住,Capitalize
必须始终返回三个值,因为这是我们定义函数的方式。有时,我们不希望处理函数可能返回的所有值。幸运的是,我们在如何在赋值端使用这些值方面有一定的灵活性。
多返回函数错误处理
当一个函数返回许多值时,Go需要我们将每个值赋给一个变量。在最后一个例子中,我们通过为capitalize
函数返回的两个值提供名称来实现这一点。这些名称应该用逗号分隔,并出现在:=
操作符的左侧。从capitalize
返回的第一个值将被赋给变量name
,第二个值(error
)将被赋给变量err
。有时,我们只对误差值感兴趣。您可以丢弃函数使用特殊变量名_
返回的任何不需要的值。
在下面的程序中,我们修改了第一个涉及capalize
函数的示例,通过传入空字符串()
来产生错误。尝试运行此程序,看看我们如何通过丢弃带有_
变量的第一个返回值来仅检查错误:
1package main
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7)
8
9func capitalize(name string) (string, error) {
10 if name == "" {
11 return "", errors.New("no name provided")
12 }
13 return strings.ToTitle(name), nil
14}
15
16func main() {
17 _, err := capitalize("")
18 if err != nil {
19 fmt.Println("Could not capitalize:", err)
20 return
21 }
22 fmt.Println("Success!")
23}
1[secondary_label Output]
2Could not capitalize: no name provided
在这次的main()
函数中,我们将大写的名称(首先返回的字符串
)赋给下划线变量(_
)。同时,我们将capalize
返回的error
赋给err
变量。然后,我们检查错误是否出现在if err!=nil
条件中。由于我们在_,err:=Capitize()
行中将一个空字符串硬编码为Capitalize
的参数,因此该条件的计算结果始终为true
。这将产生通过调用if
语句体中的fmt.Println
函数打印的输出Can‘t Capitizize:No Name Provided’。之后的
reurn将跳过
fmt.Println()`。
结论
我们已经看到了使用标准库创建错误的多种方法,以及如何以惯用的方式构建返回错误的函数。在本教程中,我们成功地使用标准库errors.New
和fmt.Errorf
函数创建了各种错误。在以后的教程中,我们将了解如何创建我们自己的自定义错误类型,以便向用户传达更丰富的信息。