程序错误在程序运行中经常发生,程序的错误又可以分为两种:一种是逻辑错误,我们一般都是可以预料到的错误,如Go中的error,出现这类错误如果不处理造成的后果一般是业务逻辑错误,处理方式一般是靠判断函数返回值来处理;另一种是堆栈错误,如Go中的panic,这种一般都不太能预先知晓,比如内存越界等错误,如果不处理会引发程序crash,这种错误大家更倾向称之为程序异常,处理方式是使用编程语言的异常捕获机制来处理。
如何区别使用 panic 和 error 两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
C中没有异常捕捉机制,如果发生数组越界程序肯定崩溃;C++中提供了try…catch…finally机制来捕获异常;而Go是提供panic来跑出异常,使用defer..recover来捕获异常。
Go的异常处理机制依赖三个内置函数:panic, defer, recover。下面就重点介绍这三个内置函数是如何配合完成一次异常处理的:
- panic抛出异常
- defer保证函数退出时最后执行的动作
- recover捕捉panic,提供控制权
panic
panic作为Go的内置函数,如果该函数被调用后,就会终止其后面要执行的代码。如果这个painc没有被捕获,那整个程序就会退出,并打印出调用栈报告错误,我们可以根据打印出来的调用栈信息定位到发生panic的具体位置。
panic中文翻译为恐慌,个人认为这个词用得很传神,当panic发生时,表示程序发生异常了,如果我们不捕获这个异常并执行相应的处理,那么恐慌就会迅速扩散直到产生严重后果—coredump。如果我们捕获了这个panic,就是意味着这个异常仍处于我们的意料之中,我们适当处理即可,不会产生严重影响。
引发panic有哪些情况呢?我总结了以下几种:
- 数组越界
- 使用空指针
- 误用互斥锁
- 对已关闭的channel发送数据
- 除0
- 主动调用了内置函数panic
如何自定义抛出一个异常?可以参考下面这个例子:
package main
// main.go
import (
"fmt"
)
func errFun() {
fmt.Println("call errFunc start")
panic("errFun panic")
fmt.Println("call errFunc end")
}
func main() {
errFun()
fmt.Println("Program finished succ!")
}
输出如下,报错打印可以看出具体发送panic的文件和代码位置,方便开发者查找问题。
call errFunc start
panic: errFun panic
goroutine 1 [running]:
main.errFun()
/mnt/e/services/gogo/main.go:11 +0x95
main.main()
/mnt/e/services/gogo/main.go:16 +0x22
exit status 2
有些panic我们并不希望捕捉它,因为有些程序的异常可能以程序退出的方式来处理反而是最优解;但并不是所有panic都可以放任不管,比如有些http请求引发的panic,不应该导致我们http server的宕机,优雅的处理方式是提前结束这个请求并报警给开发者即可。使用defer+recover的组合可以优雅捕捉到panic。
defer
当你需要在函数执行完成后再执行一段逻辑,那defer可以很好地完成这个功能,即使是panic发生时,程序无法往下执行时也会保证能走到defer函数的处理,待defer处理完后,再结束整个函数逻辑。这个特性非常好用,比如我们可以函数退出时加一些关闭操作(channel,fd等),也可以处理panic。
比如下面这个程序,我在main中加了defer操作,在main结束前会执行我们定义的defer逻辑。
package main
// main.go
import (
"fmt"
)
func errFun() {
fmt.Println("call errFunc start")
panic("errFun panic")
fmt.Println("call errFunc end")
}
func main() {
defer func() {
fmt.Println("main defer call")
} ()
errFun()
fmt.Println("Program finished succ!")
}
输出如下,可以看到"Program finished succ!"
这句没有打印,表示panic发生代码之后的逻辑都不会继续执行了,但是main defer call
打印了,说明不管如何panic,defer逻辑都会在函数退出时执行到的。
call errFunc start
main defer call
panic: errFun panic
goroutine 1 [running]:
main.errFun()
/mnt/e/services/gogo/main.go:11 +0x95
main.main()
/mnt/e/services/gogo/main.go:19 +0x48
exit status 2
recover
recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。
观察下面这个例子,这里写了两个有问题的函数,一个是divideZero,模拟除0错误,一个是getArray,模拟数组越界,都会触发panic。第一个例子触发的是数组越界错误,我们使用defer+recover的机制捕捉它。
package main
// main.go
import (
"fmt"
)
var arr [3]int
func divideZero(a,b int) int {
fmt.Println("divideZero fun in")
return a / b
}
func getArray(index, v int) {
fmt.Println("getArray fun in")
val := arr[index]
divideZero(val, v)
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("main defer call")
}
} ()
for i:=0; i<3; i++ {
arr[i] = i+1
}
//fmt.Println("call getArray1")
//getArray(2,1) // 正常
//fmt.Println("call getArray2")
getArray(3,1) //引发数组越界错误
fmt.Println("call getArray3")
//getArray(2,0) //引发除0错误
fmt.Println("Program finished succ!")
}
输出可以看出,getArray执行了,因为数组越界导致divideZero没有执行,抛出panic后被main的recover捕获了。
getArray fun in
main defer call
第二个例子,同样是调用getArray,但是没有发生数组越界,然后继续执行divideZero,发生处理异常,抛出panic,getArray没有捕获panic,因此panic继续往上抛,知道被main的recover捕获处理。
package main
// main.go
import (
"fmt"
)
var arr [3]int
func divideZero(a,b int) int {
fmt.Println("divideZero fun in")
return a / b
}
func getArray(index, v int) {
fmt.Println("getArray fun in")
val := arr[index]
divideZero(val, v)
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("main defer call")
}
} ()
for i:=0; i<3; i++ {
arr[i] = i+1
}
//fmt.Println("call getArray1")
//getArray(2,1) // 正常
//fmt.Println("call getArray2")
//getArray(3,1) //引发数组越界错误
fmt.Println("call getArray3")
getArray(2,0) //引发除0错误
fmt.Println("Program finished succ!")
}
输出
call getArray3
getArray fun in
divideZero fun in
main defer call
下面这个例子比较好得模拟了一个事件主循环,根据不同的事件v的不同执行不同的逻辑,即使是某个分支发生了panic,也不会导致程序退出。在mainLoop()和divideZero()设置了recover,当divideZero抛出panic时,divideZero自己就能做好异常处理,不会影响mainLoop逻辑,而getArray函数没有设置recover,panic时就会往上抛,直到被mainLoop的recover捕获。因为mainLoop中捕获了panic,然后mainLoop退出,在main主循环里再次启动mainLoop(),事件主循环逻辑继续执行。
package main
// main.go
import (
"fmt"
"time"
)
var arr [3]int
func divideZero(a,b int) int {
defer func() {
if err := recover(); err != nil {
fmt.Println("divideZero defer call !!!! ")
}
} ()
fmt.Println("divideZero fun in ", a, b)
return a / b
}
func getArray(index, v int) {
fmt.Println("getArray fun in ", index, v)
val := arr[index]
fmt.Println("getArray fun in2 ", index, val)
}
func mainLoop() {
defer func() {
if err := recover(); err != nil {
fmt.Println("mainLoop defer call !!!! ")
}
} ()
for {
time.Sleep(time.Second * 1)
v := time.Now().Unix() % 10
if v % 3 == 0 {
getArray(3,1) //引发数组越界错误
} else if v % 3 == 1{
divideZero(2,0) //引发除0错误
} else {
getArray(2,1) // 正常
}
}
}
func main() {
for i:=0; i<3; i++ {
arr[i] = i+1
}
for {
fmt.Println("main call mainLoop !!!! ")
mainLoop()
}
}
输出如下,不管逻辑函数怎么引发panic,都一定会被mainLoop函数中的recover捕捉到,因此程序一直可以循环执行。
main call mainLoop !!!!
divideZero fun in 2 0
divideZero defer call !!!!
getArray fun in 2 1
getArray fun in2 2 3
getArray fun in 3 1
mainLoop defer call !!!!
main call mainLoop !!!!
getArray fun in 3 1
mainLoop defer call !!!!
main call mainLoop !!!!
divideZero fun in 2 0
divideZero defer call !!!!
getArray fun in 2 1
getArray fun in2 2 3
getArray fun in 3 1
mainLoop defer call !!!!
main call mainLoop !!!!
如果我们觉得函数中某段代码有点虚,想捕捉这段代码的panic,应该怎么写呢?可以使用匿名函数完成这个需求,如下所示,匿名函数捕捉了val := arr[index]
引发的panic,恢复处理后val := arr[index]
后面的逻辑也能顺利继续进行下去了。
func getArray(index, v int) {
fmt.Println("getArray fun in ", index, v)
func() {
defer func() {
if err := recover(); err != nil {
fmt.Println("getArray defer call !!!! ")
}
} ()
val := arr[index]
fmt.Println("getArray fun in2 ", index, val)
} ()
fmt.Println("getArray fun in3 ", index, v)
}