返古的错误处理方式,是 Go 被谈及最多的内容之一。
error
官方推荐的标准做法是返回 error 状态。
func Scanln(a ...interface{}) (n int, err error)
标准库将 error 定义为接口类型,以便实现自定义错误类型。
type error interface {
Error() string
}
按惯例,error 总是最后一个返回参数。标准库提供了相关创建函数,可方便地创建包含简单错误文本的 error 对象。
var errDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, errDivByZero
}
return x / y, nil
}
func main() {
z, err := div(5, 0)
if err == errDivByZero {
log.Fatalln(err)
}
println(z)
}
应通过错误变量,而非文本内容来判定错误类别。
错误变量通常以 err 作为前缀,且字符串内容全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。
与 errors.New 类似的还有 fmt.Errorf,它返回一个格式化内容的错误对象。
某些时候,我们需要自定义错误类型,以容纳更多上下文状态信息。这样的话,还可基于类型做出判断。
type MyError struct {} // 自定义错误类型
func (*MyError) Error string { // 实现 error 接口方法
return "my error"
}
func (*MyError) Msg string {
return "my error msg"
}
func bar() {
return MyError{}
}
func main() {
err := bar()
if err != nil {
switch e := err.(type) { // 根据类型匹配
case MyError:
fmt.Println(e, e.Msg())
default:
fmt.Println(e)
}
}
}
自定义错误类型通常以 Error 为名称后缀。在用 switch 按类型匹配时,注意 case 顺序。应将自定义类型放在前面,优先匹配更具体的错误类型。
在正式代码中,我们不能忽略error返回值,应严格检查,否则可能会导致错误的逻辑状态。
调用多返回值函数时,除 error 外,其他返回值同样需要关注,以 os.File.Read 方法为例,它会同时返回剩余内容和 EOF。
大量函数和方法返回 error,使得调用代码变得很难看,一堆堆的检查语句充斥在代码行间。解决思路有:
- 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码。
- 在不影响逻辑的情况下,使用 defer 延后处理错误状态(err 退化赋值)。
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
panic,recover
与 error 相比,panic/recover 在使用方法上更接近 try/catch 结构化异常。
func panic(v interface{})
func recover() interface{}
比较有趣的是,它们是内置函数而非语句。panic 会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover 可捕获并返回 panic 提交的错误对象。
func main() {
defer func() {
if err := recover(); err != nil { // 捕获错误
log.Fatalln(err)
}
}()
panic("i am dead") // 引发错误
println("exit.") // 永不会执行
}
因为 panic 参数是空接口类型,因此可使用任何对象作为错误状态。而 recover 返回结果同样要做转型才能获得具体信息。
无论是否执行 recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。
func test() {
defer println("test.1")
defer println("test.2")
panic("i am dead")
}
func main() {
defer func() {
log.Println(recover()) // 捕获
}()
test()
}
连续调用 panic,仅最后一个会被 recover 捕获。
func main() {
defer func() {
for {
if err := recover(); err != nil {
log.Println(err)
} else {
log.Fatalln("fatal")
}
}
}()
defer func() {
panic("you are dead") // 类似重新抛出异常(rethrow)
}() // 可先recover捕获,包装后重新抛出
panic("i am dead")
}
输出:
you are dead
fatal
在延迟函数中 panic,不会影响后续延迟调用执行。而 recover 之后 panic,可被再次捕获。
另外,recover 必须在延迟调用函数中执行才能正常工作。
func catch() {
log.Println("catch:", recover())
}
func main() {
defer catch() // 捕获
defer log.Println(recover()) // 失败,输出:nil
defer recover() // 失败
panic("i am dead")
}
考虑到 recover 特性,如果要保护代码片段,那么只能将其重构为函数调用。(对比 try-catch,需要多包一层 func)
func test(x,y int) {
z := 0
func() { // 利用匿名函数try:z = x / y
defer func() {
if recover() != nil {
z = 0
}
}()
z = x / y
}()
println("x / y =", z)
}
func main() {
test(5,0)
}
调试阶段,可使用 runtime/debug.PrintStack 函数输出完整调用堆栈信息。
import "runtime/debug"
debug.PrintStack()
建议:除非是不可恢复性、导致系统无法正常工作的错误,否则不建议使用 panic。
例如:文件系统没有操作权限,服务端口被占用,数据库未启动等情况。