健壮的代码需要对意外情况做出正确的反应,如错误的用户输入、错误的网络连接和故障的磁盘。错误处理是识别程序何时处于意外状态的过程,并采取措施记录诊断信息以供以后调试。
不像其他语言要求开发人员使用特殊的语法来处理错误,Go中的错误是从函数中返回类型为error
的值,就像任何其他值一样。要在Go中处理错误,我们必须检查函数可能返回的这些错误,确定是否发生了错误,并采取适当的行动保护数据,并告诉用户或操作人员错误发生了。
创建错误
在处理错误之前,我们需要先创建一些错误。标准库提供了两个内置函数来创建错误:errors.New
和fmt.Errorf
。这两个函数都允许你指定一个自定义错误消息,稍后可以将其呈现给用户。
errors.New
接受一个参数------一个字符串形式的错误消息,你可以自定义它来警告你的用户发生了什么错误。
试着运行下面的例子,看看errors.New
创建的错误会打印到标准输出:
go
package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("barnacles")
fmt.Println("Sammy says:", err)
}
shell
OutputSammy says: barnacles
我们使用标准库中的errors.New
函数创建了一个新的错误消息,字符串"barnacles"
作为错误消息。我们在这里遵循惯例,按照Go编程语言风格指南的建议,使用小写字母表示错误消息。
最后,我们使用fmt.Println
函数将错误消息与"Sammy says:"
结合起来。
fmt.Errorf
函数允许你动态构建错误消息。它的第一个参数是一个包含错误消息的字符串,其中包含占位符值,例如字符串为%s
,整数为%d
。fmt.Errorf
将格式化字符串后面的参数按顺序插入到这些占位符中:
go
package main
import (
"fmt"
"time"
)
func main() {
err := fmt.Errorf("error occurred at: %v", time.Now())
fmt.Println("An error happened:", err)
}
shell
OutputAn 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
------一个未初始化的值。
下面的例子包含了一个总是返回错误的函数。请注意,当你运行这个程序时,尽管函数这次返回了错误,但它的输出与前一个例子相同。在不同的位置声明错误并不会改变错误消息。
go
package main
import (
"errors"
"fmt"
)
func boom() error {
return errors.New("barnacles")
}
func main() {
err := boom()
if err != nil {
fmt.Println("An error occurred:", err)
return
}
fmt.Println("Anchors away!")
}
shell
OutputAn error occurred: barnacles
这里我们定义了一个名为boom()
的函数,它返回一个我们使用errors.New
构造的error
。然后我们调用这个函数并使用err:= boom()
捕获错误。一旦我们给这个错误赋值,我们就用条件语句if err != nil
检查它是否存在。这里的条件将总是求值为true
,因为我们总是从boom()
返回一个error
。
但情况并不总是如此,所以最好让逻辑处理不存在错误(nil
)和存在错误的两种情况。当出现错误时,我们使用fmt.Println
来打印错误和前缀,就像我们在前面的示例中所做的那样。最后,我们使用return
语句来跳过fmt.Println("Anchors away!")
的执行,因为它只应该在没有错误发生时执行。
**注意:**最后一个例子中的if err != nil
构造是Go编程语言中错误处理的主力。在函数可能产生错误的地方,使用if
语句来检查是否发生错误是很重要的。通过这种方式,惯用的Go代码自然在第一个缩进级别拥有它的"happy path"逻辑,在第二个缩进级别拥有所有的"sad path"逻辑。
If语句有一个可选的赋值子句,可用于简化函数调用和错误处理。
运行下一个程序,会看到与之前示例相同的输出,但这次使用了复合if
语句来减少一些样板代码:
go
package main
import (
"errors"
"fmt"
)
func boom() error {
return errors.New("barnacles")
}
func main() {
if err := boom(); err != nil {
fmt.Println("An error occurred:", err)
return
}
fmt.Println("Anchors away!")
}
shell
OutputAn error occurred: barnacles
和之前一样,我们有一个总是返回错误的函数boom()
。我们将boom()
返回的错误赋值给err
,作为if
语句的第一部分。在if
语句的第二部分,分号之后,err
变量是可用的。我们检查错误是否存在,并像之前那样用短前缀字符串打印错误。
在本节中,我们学习了如何处理只返回错误的函数。这些函数很常见,但能够处理返回多个值的函数的错误也很重要。
在值旁边返回错误
返回单个错误值的函数通常会影响一些有状态的更改,比如向数据库中插入行。编写函数时,如果成功返回一个值,如果失败则返回一个潜在的错误,这种情况也很常见。Go允许函数返回多个结果,可用于同时返回值和错误类型。
要创建返回多个值的函数,可以在函数签名的括号中列出每个返回值的类型。例如,一个返回string
和error
的capitalize
函数可以使用func capitalize(name string) (string, error){}
来声明。(string, error)
部分告诉Go编译器,这个函数将返回一个string
和一个error
,按照这个顺序。
运行下面的程序,看看这个同时返回string
和error
的函数的输出:
go
package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
name, err := capitalize("sammy")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Capitalized name:", name)
}
shell
OutputCapitalized name: SAMMY
我们将capitalize()
定义为一个函数,它接受一个字符串(要大写的名字)作为参数,并返回一个字符串和一个错误值。在main()
中,我们调用capitalize()
,并将函数返回的两个值赋值给name
和err
变量,方法是在:=
操作符的左边用逗号分隔它们。在这之后,我们执行if err != nil
检查,就像前面的例子一样,如果错误存在,使用fmt.Println
将错误打印到标准输出。如果没有报错,我们打印Capitalized name: SAMMY
。
试着将name, err := capitalize("sammy")
中的字符串"sammy"
改为空字符串("")
,你将得到错误信息Could not capitalize: no name provided
。
当调用者为name
参数提供空字符串时,capitalize
函数将返回错误。当name
参数不是空字符串时,capitalize()
使用strings.ToTitle
将name
参数大写,并返回nil
作为错误值。
此示例遵循一些微妙的约定,这些约定是典型的Go代码,但不是Go编译器强制执行的。当一个函数返回多个值,包括一个error时,按照约定我们会返回error
作为最后一项。当从具有多个返回值的函数返回error
时,惯用的Go代码也会将每个非错误值设置为零值 。例如,0
表示字符串的空字符串,0
表示整数,struct
表示结构类型,nil
表示接口和指针类型,等等。我们在关于变量和常量的教程中更详细地介绍了零值
减少样板
在函数需要返回很多值的情况下,遵守这些约定会变得很乏味。我们可以使用匿名函数来帮助减少样板代码。匿名函数是被赋值给变量的过程。与前面示例中定义的函数不同,它们只在声明它们的函数中可用,这使它们成为可重用的辅助逻辑片段。
下面的程序修改了最后一个示例,使其包含我们要大写的名称的长度。由于它有三个返回值,如果没有匿名函数的帮助,处理错误可能会变得很麻烦:
go
package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, int, error) {
handle := func(err error) (string, int, error) {
return "", 0, err
}
if name == "" {
return handle(errors.New("no name provided"))
}
return strings.ToTitle(name), len(name), nil
}
func main() {
name, size, err := capitalize("sammy")
if err != nil {
fmt.Println("An error occurred:", err)
}
fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
shell
OutputCapitalized name: SAMMY, length: 5
在main()
中,我们现在捕获capitalize
返回的三个参数,分别为name
、size
和err
。然后,我们通过检查err
变量是否不等于nil
来检查capitalize
是否返回了error
。在尝试使用capitalize
返回的任何其他值之前,这一点很重要,因为匿名函数handle
可能会将这些值设置为0。因为我们提供了字符串' "sammy" ',所以没有发生错误,所以我们打印出了首字母大写的名字和它的长度。
同样,你可以尝试将"sammy"
改为空字符串("")
,以查看打印的错误情况(An error occurred: no name provided
)。
在capitalize
中,我们将handle
变量定义为一个匿名函数。它接受一个错误,并以与capitalize
相同的顺序返回相同的值。handle
将这些值设置为零,并将作为参数传递的error
作为最终返回值。使用它,我们可以通过在调用handle
之前使用return
语句将error
作为其参数来返回在capitalize
中遇到的任何错误。
请记住,capitalize
必须始终返回三个值,因为这就是我们定义函数的方式。有时我们并不想处理函数可能返回的所有值。幸运的是,在赋值方面,我们可以灵活地使用这些值。
处理来自多重返回函数的错误
当函数返回许多值时,Go要求我们将每个值分配给一个变量。在上一个例子中,我们通过为capitalize
函数返回的两个值提供名称来实现这一点。这些名称应该用逗号分隔,并出现在:=
操作符的左侧。从capitalize
返回的第一个值将被赋值给name
变量,第二个值(error
)将被赋值给变量err
。有时候,我们只对误差值感兴趣。你可以使用特殊的_
变量名丢弃函数返回的任何不需要的值。
在下面的程序中,我们修改了第一个涉及capitalize
函数的例子,通过传递空字符串("")
来产生错误。试着运行这个程序,看看我们如何通过丢弃_
变量的第一个返回值来检查错误:
go
package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
_, err := capitalize("")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Success!")
}
shell
OutputCould not capitalize: no name provided
这次在main()
函数中,我们将首字母大写的名字(首先返回的string
)赋值给下划线变量(_
)。同时,我们将capitalize
返回的error
赋值给err
变量。然后我们在条件语句if err != nil
中检查错误是否存在。因为我们硬编码了一个空字符串作为capitalize
在_, err:= capitalize("")
这一行的参数,所以这个条件语句总是被求值为true
。这产生了在if
语句体中调用fmt.Println
函数输出的"Could not capitalize: no name provided"
。之后的return
将跳过fmt.Println("Success!")
。
总结
我们已经看到了使用标准库创建错误的多种方式,以及如何以惯用的方式构建返回错误的函数。在本教程中,我们使用标准库的errors.New
和fmt.Errorf
函数成功地创建了各种错误。在以后的教程中,我们将看看如何创建我们自己的自定义错误类型,以向用户传递更丰富的信息。