错误处理与测试
Go 没有像 Java 和 .NET 那样的 try/catch
异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover
机制。
Go 的设计者觉得 try/catch
机制的使用太泛滥了,而且从底层向更高的层级抛异常太耗费资源。他们给 Go 设计的机制也可以 “捕捉” 异常,但是更轻量,并且只应该作为(处理错误的)最后的手段。
Go 检查和报告错误条件的惯有方式:
产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是
nil
就是成功,非nil
就是发生了错误。为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
错误处理
Go 有一个预先定义的 error
接口类型
1 | type error interface { |
错误值用来表示异常状态;errors
包中有一个errorString
结构体实现了 error
接口。当程序处于错误状态时可以用 os.Exit(1)
来中止运行。
定义错误
任何时候当你需要一个新的错误类型,都可以用 errors
(必须先 import)包的 errors.New
函数接收合适的错误信息来创建,像下面这样:
1 | err := errors.New(“math - square root of negative number”) |
1 | package main |
可以把它用于计算平方根函数的参数测试:
1 | func Sqrt(f float64) (float64, error) { |
你可以像下面这样调用 Sqrt
函数:
1 | if f, err := Sqrt(-1); err != nil { |
用 fmt 创建错误对象
通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf()
来实现:它和fmt.Printf()
完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用:
1 | if f < 0 { |
运行时异常和 panic
当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic
,伴随着程序的崩溃抛出一个 runtime.Error
接口类型的值。这个错误值有个RuntimeError()
方法用于区别普通错误。
panic
可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic
函数产生一个中止程序的运行时错误。panic
接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。
从 panic 中恢复(Recover)
正如名字一样,这个(recover)内建函数被用于从 panic
或 错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。
recover
只能在 defer 修饰的函数中使用:用于取得panic
调用中传递过来的错误值,如果是正常执行,调用 recover
会返回 nil
,且没有其它效果。
总结:panic
会导致栈被展开直到 defer
修饰的 recover()
被调用或者程序中止。
下面例子中的 protect 函数调用函数参数 g 来保护调用者防止从 g 中抛出的运行时 panic,并展示 panic 中的信息:1
2
3
4
5
6
7
8
9
10
11func protect(g func()) {
defer func() {
log.Println("done")
// Println executes normally even if there is a panic
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // possible runtime-error
}
defer-panic-recover
在某种意义上也是一种像 if
,for
这样的控制流机制。
Go 中的单元测试和基准测试
首先所有的包都应该有一定的必要文档,然后同样重要的是对包的测试。
名为 testing 的包被专门用来进行自动化测试,日志和错误报告。并且还包含一些基准测试函数的功能。
对一个包做(单元)测试,需要写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性。于是我们必须写一些 Go 源文件来测试代码。测试程序必须属于被测试的包,并且文件名满足这种形式 *_test.go
,所以测试代码和包中的业务代码是分开的。
_test
程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 gotest
会编译所有的程序:普通程序和测试程序。
测试文件中必须导入 “testing” 包,并写一些名字以TestZzz
打头的全局函数,这里的 Zzz
是被测试函数的字母描述,如 TestFmtInterface
,TestPayEmployees
等。
测试函数必须有这种形式的头部:
1 | func TestAbcde(t *testing.T) |
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log
,t.Error
,t.ErrorF
等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。
用下面这些函数来通知测试失败:
1) 标记测试函数为失败,然后继续执行(剩下的测试)。
1 | func (t *T) Fail() |
2) 标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。
1 | func (t *T) FailNow() |
3) args
被用默认的格式格式化并打印到错误日志中。
1 | func (t *T) Log(args ...interface{}) |
4) 结合 先执行 3),然后执行 2)的效果。
1 | func (t *T) Fatal(args ...interface{}) |
运行 go test
来编译测试程序,并执行程序中所有的TestZZZ
函数。如果所有的测试都通过会打印出 PASS。
gotest
可以接收一个或多个函数程序作为参数,并指定一些选项。
结合 --chatty
或 -v
选项,每个执行的测试函数以及测试状态会被打印。
testing
包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz
打头的函数并接收一个 *testing.B
类型的参数,比如:
1 | func BenchmarkReverse(b *testing.B) { |
命令 go test –test.bench=.*
会运行所有的基准测试函数;代码中的函数会被调用 N 次(N是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark
调用这些函数,直接运行程序即可。