三十一.golang的panic 和 recover
在 Go 语言中,程序中一般是使用[错误]来处理异常情况。对于程序中出现的大部分异常情况,错误就已经够用了。
但在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当[函数]发生 panic 时,它会终止运行,在执行完所有的[延迟]函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前[协程]的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。在编写一个示例程序后,我们就能很好地理解这个概念了。
在本教程里,我们还会接着讨论,当程序发生 panic 时,使用 recover
可以认为 panic 和 recover 与其他语言中的 try-catch-finally 语句类似,只不过一般我们很少使用 panic 和 recover。而当我们使用了 panic 和 recover 时,也会比 try-catch-finally 更加优雅,代码更加整洁。
需要注意的是,你应该尽可能地使用错误,而不是使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制。
panic 有两个合理的用例。
-
-
发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。
func panic(interface{})
我们会写一个例子,来展示 panic 如何工作。
package main import ( "fmt" ) func fullName(firstName *string, lastName *string) { if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main") }
上面的程序很简单,会打印一个人的全名。第 7 行的 fullName 函数会打印出一个人的全名。该函数在第 8 行和第 11 行分别检查了 firstName 和 lastName 的指针是否为 nil。如果是 nil,fullName 函数会调用含有不同的错误信息的 panic。当程序终止时,会打印出该错误信息。
panic: runtime error: last name cannot be nil goroutine 1 [running]: main.fullName(0x1040c128, 0x0) /tmp/sandbox135038844/main.go:12 +0x120 main.main() /tmp/sandbox135038844/main.go:20 +0x80
我们来分析这个输出,理解一下 panic 是如何工作的,并且思考当程序发生 panic 时,会怎样打印堆栈跟踪。
在第 19 行,我们将
panic: runtime error: last name cannot be empty
接着打印出堆栈跟踪。
程序在 fullName
main.fullName(0x1040c128, 0x0)
/tmp/sandbox135038844/main.go:12 +0x120
main.main()
/tmp/sandbox135038844/main.go:20 +0x80
现在我们已经到达了导致 panic 的顶层函数,这里没有更多的层级,因此结束打印。
我们重新总结一下 panic 做了什么。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。
在上面的例子中,我们没有延迟调用任何函数。如果有延迟函数,会先调用它,然后程序控制返回到函数调用方。
我们来修改上面的示例,使用一个延迟语句。
package main import ( "fmt" ) func fullName(firstName *string, lastName *string) { defer fmt.Println("deferred call in fullName") if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { defer fmt.Println("deferred call in main") firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main") }
上述代码中,我们只修改了两处,分别在第 8 行和第 20 行添加了延迟函数的调用。
该函数会打印:
This program prints, deferred call in fullName deferred call in main panic: runtime error: last name cannot be nil goroutine 1 [running]: main.fullName(0x1042bf90, 0x0) /tmp/sandbox060731990/main.go:13 +0x280 main.main() /tmp/sandbox060731990/main.go:22 +0xc0
当程序在第 13 行发生 panic 时,首先执行了延迟函数,接着控制返回到函数调用方,调用方的延迟函数继续运行,直到到达顶层调用函数。
deferred call in fullName
deferred call in main
现在程序控制到达了顶层函数,因此该函数会打印出 panic 信息,然后是堆栈跟踪,最后终止程序。
recover 是一个内建函数,用于重新获得 panic 协程的控制。
recover 函数的标签如下所示:
func recover() interface{}
只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic
我们来修改一下程序,在发生 panic 之后,使用 recover 来恢复正常的运行。
package main import ( "fmt" ) func recoverName() { if r := recover(); r!= nil { fmt.Println("recovered from ", r) } } func fullName(firstName *string, lastName *string) { defer recoverName() if firstName == nil { panic("runtime error: first name cannot be nil") } if lastName == nil { panic("runtime error: last name cannot be nil") } fmt.Printf("%s %s\n", *firstName, *lastName) fmt.Println("returned normally from fullName") } func main() { defer fmt.Println("deferred call in main") firstName := "Elon" fullName(&firstName, nil) fmt.Println("returned normally from main") }
在第 7 行,recoverName() 函数调用了 recover(),返回了调用 panic 的传参。在这里,我们只是打印出 recover 的返回值(第 8 行)。在 fullName 函数内,我们在第 14 行延迟调用了 recoverNames()。
当 fullName 发生 panic 时,会调用延迟函数 recoverName(),它使用了 recover() 来停止 panic 续发事件。
recovered from runtime error: last name cannot be nil returned normally from main deferred call in main
recovered from runtime error: last name cannot be nil
package main import ( "fmt" "time" ) func recovery() { if r := recover(); r != nil { fmt.Println("recovered:", r) } } func a() { defer recovery() fmt.Println("Inside A") go b() time.Sleep(1 * time.Second) } func b() { fmt.Println("Inside B") panic("oh! B panicked") } func main() { a() fmt.Println("normally returned from main") }
在上面的程序中,函数 b() 在第 23 行发生 panic。函数 a() 调用了一个延迟函数 recovery(),用于恢复 panic。在第 17 行,函数 b() 作为一个不同的协程来调用。下一行的 Sleep 只是保证 a() 在 b() 运行结束之后才退出。
你认为程序会输出什么?panic 能够恢复吗?答案是否定的,panic 并不会恢复。因为调用 recovery 的协程和 b() 中发生 panic 的协程并不相同,因此不可能恢复 panic。
运行该程序会输出:
Inside A Inside B panic: oh! B panicked goroutine 5 [running]: main.b() /tmp/sandbox388039916/main.go:23 +0x80 created by main.a /tmp/sandbox388039916/main.go:17 +0xc0
如果函数 b() 在相同的协程里调用,panic 就可以恢复。
如果程序的第 17 行由 go b() 修改为 b(),就可以恢复 panic 了,因为 panic 发生在与 recover 相同的协程里。如果运行这个修改后的程序,会输出:
Inside A Inside B recovered: oh! B panicked normally returned from main
type Error interface { error // RuntimeError is a no-op function but // serves to distinguish types that are run time // errors from ordinary errors: a type is a // run time error if it has a RuntimeError method. RuntimeError() }
而 runtime.Error 接口满足内建接口类型 [error]。
package main import ( "fmt" ) func a() { n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a") } func main() { a() fmt.Println("normally returned from main") }
panic: runtime error: index out of range goroutine 1 [running]: main.a() /tmp/sandbox780439659/main.go:9 +0x40 main.main() /tmp/sandbox780439659/main.go:13 +0x20
你也许想知道,是否可以恢复一个运行时 panic?当然可以!我们来修改一下上面的代码,恢复这个 panic。
package main import ( "fmt" ) func r() { if r := recover(); r != nil { fmt.Println("Recovered", r) } } func a() { defer r() n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a") } func main() { a() fmt.Println("normally returned from main") }
运行上面程序会输出:
Recovered runtime error: index out of range normally returned from main
从输出可以知道,我们已经恢复了这个 panic。
有办法可以打印出堆栈跟踪,就是使用 [Debug]包中的 [PrintStack]函数。
package main import ( "fmt" "runtime/debug" ) func r() { if r := recover(); r != nil { fmt.Println("Recovered", r) debug.PrintStack() } } func a() { defer r() n := []int{5, 7, 4} fmt.Println(n[3]) fmt.Println("normally returned from a") } func main() { a() fmt.Println("normally returned from main") }
在上面的程序中,我们在第 11 行使用了 debug.PrintStack() 打印堆栈跟踪。
Recovered runtime error: index out of range goroutine 1 [running]: runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c) /usr/local/go/src/runtime/debug/stack.go:24 +0xc0 runtime/debug.PrintStack() /usr/local/go/src/runtime/debug/stack.go:16 +0x20 main.r() /tmp/sandbox949178097/main.go:11 +0xe0 panic(0xf0a80, 0x17cd50) /usr/local/go/src/runtime/panic.go:491 +0x2c0 main.a() /tmp/sandbox949178097/main.go:18 +0x80 main.main() /tmp/sandbox949178097/main.go:23 +0x20 normally returned from main
从输出我们可以看出,首先已经恢复了 panic,打印出 Recovered runtime error: index out of range。此外,我们也打印出了堆栈跟踪。在恢复了 panic 之后,还打印出 normally returned from main。
简单概括一下本教程讨论的内容:
-
什么是 panic?
-
什么时候应该使用 panic?
-
panic 示例
-
发生 panic 时的 defer
-
recover
-
panic,recover 和 Go 协程
-
运行时 panic
-
恢复后获得堆栈跟踪