在 Go 中处理错误

健壮的代码需要对用户输入错误、网络连接故障和磁盘故障等意外情况做出正确反应。_错误处理_是识别程序何时处于意外状态,并采取步骤记录诊断信息以供以后调试的过程。

与其他需要开发人员使用专门语法处理错误的语言不同,Go中的错误是从函数返回的error类型的值,就像其他任何值一样。要处理围棋中的错误,我们必须检查这些函数可能返回的错误,确定是否发生了错误,并采取适当的操作来保护数据,并告诉用户或操作员发生了错误。

创建错误

在我们可以处理错误之前,我们需要先创建一些错误。标准库提供了两个内置的创建错误的函数:errors.Newfmt.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和一个errorcapitalize函数将使用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(),并将函数返回的两个值赋给nameerr变量,方法是在:=运算符的左侧用逗号分隔它们。在此之后,我们像前面的示例一样执行if err!=nil检查,如果出现错误,则使用fmt.Println将错误输出到标准输出。如果没有错误,我们会打印大写名称:SAMMY

尝试将name中的字符串),则会收到错误Ccan‘t Capitalize:no name Provided`。

capalize函数的调用者为name参数提供空字符串时,函数将返回错误。当name参数不是空字符串时,Capitalize()会使用strass.ToTitlename参数大写,并返回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返回的三个参数,分别为namesizeerr。然后,我们通过检查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.Newfmt.Errorf函数创建了各种错误。在以后的教程中,我们将了解如何创建我们自己的自定义错误类型,以便向用户传达更丰富的信息。

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