错误处理是使用任何编程语言都需要考虑的一个重要因素,因为软件在执行过程中,难免发生错误。许多编程语言在发生错误或捕获错误时抛出异常,Go 语言却提供了一种有趣的方式处理错误:它将错误定义为一种类型。这样可以将错误通过函数或方法传递。Go 语言处理错误的思路是:通过最后一个返回值将错误返回给调用者,由调用者决定如何处理错误。Go 语言还引入了三个关键字用于标准的错误处理流程,这三个关键字分别是 defer、panic、recover。
作为有经验的程序员,要尽量考虑程序可能发生的错误,并提前编写好处理错误的代码。就像你准备面试时,会提前考虑风险,这样即使公共汽车出现了故障,也不会影响到你准时参加面试,因为你已提前做了准备。
总之,错误处理是编写健壮、易维护且可靠代码的重要组成部分。如果独力开发项目,它可以帮助你处理意外情况;如果编写开源项目,错误处理是创建值得信赖的代码的一部分。
9.1 错误类型
Go 语言中的错误是一个接口类型,声明如下:
type error interface {
Error() string
}
error 接口提供了一个名为 Error 的方法,方法返回一个字符串类型。我们看看 Error 方法是如何定义的:
package errors
type errorString struct {
text string
}
func New(text string) error {
return &errorString(text)
}
func (e *errorString) Error() string {
return e.text
}
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
9.2 创建 error
通过以上源码,可知创建 error 的两种方式:
err1 := errors.New("This is err1")
//格式化错误
err2 := fmt.Errorf("%s", "This is err2")
9.3 错误处理
当调用方法或函数失败时,Go 语言的规范之一是将错误类型作为最后一个值返回。也就是说,如果函数调用报错,错误不会在函数内引发任何异常,而是将异常返回给调用者,由调用者决定如何处理错误。如下所示,当错误发生时,ioutil.Readfile 函数会返回错误:
package main
import (
"fmt"
"io/ioutil"
)
func main() {
file, err := ioutil.ReadFile("file.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%s", file)
}
虽然代码量很少,但它表达了 Go 语言处理错误的思想。
代码释义如下:
使用 io/ioutil 标准包中的 ReadFile 函数读取文件;
如果 err 不为 nil,说明有错误发生
打印 err,并退出程序
如果 err 为 nil,打印文件内容
ReadFile 函数定义如下:
func ReadFile(filename string) ([]byte, error)
它接受一个字符串类型的参数,并返回一个字节切片和一个 error 类型,理解这些,有助于我们了解 Go 语言是如何处理错误的。
调用 ReadFile 函数总能收到一个 error 类型的值,本例 main 函数的第一行,ReadFile 函数的返回值赋给了变量 file 和 err,这是 Go 语言的常见写法。在其他开源项目中,你也可能会看到这些写法:
var file []byte
var err error
file, err = ioutil.ReadFile("files.txt")
如果在执行中没有发生错误,则 error 值为 nil。我们可以在调用方法或函数时,检查 error 类型的值,来判断程序是否按预期执行。
if err != nil {
// something went wrong.
}
这种写法在 Go 语言中很常见,可能会有些开发者觉得这种方式过于繁琐,因为每次调用方法或函数时都需要检查错误。确实如此,但在处理错误方面,Go 语言仍然比其他语言提供了更多的灵活性,因为错误可以像其他类型一样在函数中传递,这可以帮我们显著缩短代码量。
9.4 从函数返回 error
在本章不止一次提到,Go 语言处理错误的思想是将错误作为函数或方法的最后一个返回值,这个示例,介绍下如何创建和返回错误:
package main
import "fmt"
func Division(x, y int) (int, error) {
if y == 0 {
return -1, fmt.Errorf("The divisor %d cannot be 0", y)
}
return x / y, nil
}
func main() {
n, err := Division(16, 0)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(n)
}
示例中定义了一个返回 error 类型的函数 Division,以及调用者是如何处理错误的。代码释义如下:
整数16和0传递给 Division 函数;
Division 函数使用比较运算符,检查0是否等于0;
Division 函数返回-1和 error 类型的值;
调用者检查到 err 不为 nil,输出错误并执行 return 语句。
这个示例体现了 Go 语言的错误处理思想:错误处理不是在函数中发生,而是在调用函数的位置。这使得在处理错误时具有更大的灵活性,而不是采用一刀切的方法。
9.5 错误和可用性
在定义错误提示信息时,一定要以用户为中心,因为精确的错误能有效提高程序可用性。其他人可能引用你开发的代码,并希望在遇到错误时,能快速从中恢复。考虑以下错误,根据该提示,你认为这能让你轻松处理或从错误中恢复吗?
Ha Ha Ha, This is error!请自求多福。
很显然,这是一个很糟糕的错误消息,因为它没有提供错误发生的线索,也没有提供如何恢复该错误的建议。再看一个正面的例子:
No config file found. Please create one at ~/.kube/config.
这是一个相对更好的错误提示:
具体说明了导致错误的原因;
提供了解决办法
体现了以用户为中心
想让你的代码得到更多人的信赖,那么以一致的方式返回错误和有用的信息是必不可少的。
9.6 避免使用 Panic
panic 是 Go 的内置函数,在发生 panic 后,会停止正常的控制流程,并终止程序运行。对于正常的错误,是不建议使用的。以下示例演示了 panic 是如何终止程序的:
package main
import (
"fmt"
)
func main() {
fmt.Println("This is executed")
panic("Oh no. I can do no more. GoodBye.")
fmt.Println("This is not executed")
}
这个程序导致了恐慌和程序崩溃。执行到第二句的时候,panic 被调用,”This is not executed”永远不会被执行。
使用 panic 的场景:
程序遇到不可恢复的错误。这可能是状态丢失,或者继续执行程序将导致更多的问题。在这种情况下,最好的做法是终止程序;
错误无法被处理。
9.7 延迟执行
defer 关键字用来实现延迟执行,它会在函数结束前被执行。常用于关闭打开的文件,释放资源等,也会结合 recover 函数,捕获 panic。
defer 语句后面必须是一个函数:
defer fmt.Println("I'm defer")
9.8 recover
panic 属于致命错误,会导致程序崩溃,我们可以通过 recover 函数,在 panic 出现时避免程序崩溃。
recover 函数放在 defer 调用的函数中才会有效。
一个超过索引范围的例子:
package main
import (
"fmt"
)
func LoopArray(s1 [3]int) {
for i := 0; i < 4; i++ {
fmt.Println(s1[i])
}
}
func main() {
s := [3]int{0, 1, 2}
LoopArray(s)
fmt.Println("Done.")
}
代码释义:
定义一个长度为3,类型为 int 的数组 s1
s1 的索引下标为0,1,2
使用 for 语句遍历数组,遍历范围是0,1,2,3
超过了索引范围,打印出 s1 的值后,程序会报 panic 错误,并结束执行
Done. 并不会被打印
使用 recover 捕获 panic,让程序继续:
package main
import (
"fmt"
)
func LoopArray(s1 [3]int) {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
for i := 0; i < 4; i++ {
fmt.Println(s1[i])
}
}
func main() {
s := [3]int{0, 1, 2}
LoopArray(s)
fmt.Println("Done.")
}
我在 LoopArray 函数中,使用了 recover 函数捕获 panic,此时 panic 的错误信息会被打印,便于开发者排查错误,重要的是,”Done.”被正常打印了。
9.9 小结
本章主要介绍了 Go 语言处理错误的思想,它将错误作为函数或方法的最后一个值,返回给调用者,由调用者决定如何处理错误。介绍了错误创建的两种方式,以及如何使用 recover 让程序从 panic 中恢复。