错误处理是学习任何编程语言都需要考虑的一个重要话题。在早期的语言中,错误处理不是语言规范的一部分,通常只作为一种编程范式存在,比如 C 语言中的 errno。但自 C++ 语言以来,语言层面上会增加错误处理的支持,比如异常(exception)的概念和 try-catch 关键字的引入。Go 语言在此功能上考虑得更为深远。漂亮的错误处理规范是 Go 语言最大的亮点之一。
error 接口
Go 语言引入了一个关于错误处理的标准模式,即 error
接口,该接口的定义如下:
type error interface {
Error() string
}
对于大多数函数,如果要返回错误,大致上都可以定义为如下模式,将 error 作为多种返回值中的最后一个,但这并非是强制要求:
func Foo(param int)(n int, err error) {
// ...
}
调用时的代码建议按如下方式处理错误情况:
n, err := Foo(0)
if err != nil {
// 错误处理
} else {
// 使用返回值n
}
下面使用 Go 库中的实际代码来示范如何使用自定义的 error 类型。
首先,定义一个用于承载错误信息的类型。因为 Go 语言中接口的灵活性,你根本不需要从 error 接口继承或者像 Java 一样需要使用 implements 来明确指定类型和接口之间的关系,具体代码如下:
type PathError struct {
Op string
Path string
Err error
}
如果这样的话,编译器又怎能知道 PathError 可以当一个 error 来传递呢?关键在于下面的代码实现了 Error() 方法:
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
在 Go 中,只要一个 struct 实现了某一个接口的所有方法,那么 Go 编译器就认为其实现了对应的接口,因为 error 接口就一个 Error() 方法,所以这里 PathError 实现了 Error() 方法之后就相当于实现了 error 接口 。之后就可以直接返回 PathError 变量了,比如在下面的代码中,当 syscall.Stat() 失败返回 err 时,将该 err 包装到一个 PathError 对象中返回:
func Stat(name string) (fi FileInfo, err error) {
var stat syscall.Stat_t
err = syscall.Stat(name, &stat)
if err != nil {
return nil, &PathError{"stat", name, err}
}
return fileInfoFromStat(&stat, name), nil
}
如果在处理错误时获取详细信息,而不仅仅满足于打印一句错误信息,那就需要用到类型转换知识了:
fi, err := os.Stat("a.txt")
if err != nil {
if e, ok := err.(*os.PathError); ok && e.Err != nil {
// 获取 PathError 类型变量 e 中的其他信息并处理
}
}
这就是 Go 中 error 类型的使用方法。与其他语言中的异常相比,Go 的处理相对比较直观、简单。
defer 关键字
关键字 defer
是 Go 语言引入的一个非常有意思的特性,相信很多 C++ 程序员都写过类似下面这样的代码:
class file_closer {
FILE _f;
public:
file_closer(FILE f) : _f(f) {}
~file_closer() { if (f) fclose(f); }
};
然后在需要使用的地方这么写:
void f() {
FILE f = open_file("file.txt"); // 打开一个文件句柄
file_closer _closer(f);
// 对f句柄进行操作
}
为什么需要 file_closer 这么个包装类呢?因为如果没有这个类,代码中所有退出函数的环节,比如每一个可能抛出异常的地方,每一个 return 的位置,都需要关掉之前打开的文件句柄。即使你头脑清晰,想明白了每一个分支和可能出错的条件,在该关闭的地方都关闭了,怎么保证你的后继者也能做到同样水平?大量莫名其妙的问题就出现了。
在 C/C++ 中还有另一种解决方案。开发者可以将需要释放的资源变量都声明在函数的开头部分,并在函数的末尾部分统一释放资源。函数需要退出时,就必须使用 goto语 句跳转到指定位置先完成资源清理工作,而不能调用 return 语句直接返回。
这种方案是可行的,也仍然在被使用着,但存在非常大的维护性问题。而 Go 语言使用 defer 关键字简简单单地解决了这个问题,比如以下的例子:
func CopyFile(dst, src string) (w int64, err error) {
srcFile, err := os.Open(src)
if err != nil {
return
}
defer srcFile.Close()
dstFile, err := os.Create(dstName)
if err != nil {
return
}
defer dstFile.Close()
return io.Copy(dstFile, srcFile)
}
即使其中的 Copy() 函数抛出异常,Go 仍然会保证 dstFile 和 srcFile 会被正常关闭。如果觉得一句话干不完清理的工作,也可以使用在 defer 后加一个匿名函数的做法:
defer func() {
// 做你复杂的清理工作
} ()
另外,一个函数中可以存在多个 defer 语句,因此需要注意的是,defer 语句的调用是遵照先进后出的原则,即最后一个 defer 语句将最先被执行。只不过,当你需要为 defer 语句到底哪个先执行这种细节而烦恼的时候,说明你的代码架构可能需要调整一下了。
panic() 和 recover()
Go 语言引入了两个内置函数 panic()
和 recover()
以报告和处理运行时错误和程序中的错误场景:
func panic(interface{})
func recover() interface{}
当在一个函数执行过程中调用 panic() 函数时,正常的函数执行流程将立即终止,但函数中之前使用 defer 关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行 panic 流程,直至所属的 goroutine 中所有正在执行的函数被终止。错误信息将被报告,包括在调用 panic() 函数时传入的参数,这个过程称为错误处理流程。
从 panic() 的参数类型 interface{} 我们可以得知,该函数接收任意类型的数据,比如整型、字符串、对象等。调用方法很简单,下面为几个例子:
panic(404)
panic("network broken")
panic(Error("file not exists"))
recover() 函数用于终止错误处理流程。一般情况下,recover() 应该在一个使用 defer 关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的 goroutine 中明确调用恢复过程(使用 recover 关键字),会导致该 goroutine 所属的进程打印异常信息后直接退出。
以下为一个常见的场景。
我们对于 foo() 函数的执行要么心里没底感觉可能会触发错误处理,或者自己在其中明确加入了按特定条件触发错误处理的语句,那么可以用如下方式在调用代码中截取 recover():
defer func() {
if r := recover(); r != nil {
log.Printf("Runtime error caught: %v", r)
}
}()
foo()
无论 foo() 中是否触发了错误处理流程,该匿名 defer 函数都将在函数退出时得到执行。假如 foo() 中触发了错误处理流程,recover() 函数执行将使得该错误处理过程终止。如果错误处理流程被触发时,程序传给 panic 函数的参数不为 nil,则该函数还会打印详细的错误信息。
文章评论