Go语言中常见100问题-#48 panic
本系列文章来自书籍<<100 Go Mistakes and How to Avoid Them>>. 该书总结了Go语言中常见的100个错误,分析了每个错误的场景并给出了最佳实践。
本文来自书中的问题#48,讨论的是panic问题。对于Go语言新手来说,在使用error的时候存在一些疑惑。在Go语言中,error通常作为方法或函数的最后一个返回参数。有人可能想采用Java或Python语言中的panic和recover机制来处理异常。Go中也是有异常捕获recover机制的,现在来我们来看在什么场景下使用panic和recover.
panic会终止代码执行逻辑
panic语句会终止代码执行,即fmt.Println("b")不会被执行
.
func main() {
fmt.Println("a")
panic("foo")
fmt.Println("b")
// panic之后的内容不会输出,类似于linux中的exit函数功能,程序直接退出
}
上面程序输出如下:
a
panic: foo
goroutine 1 [running]:
main.main()
main.go:7 +0x95
exit status 2
panic异常执行流程
panic语句被执行后,异常执行流程有两种情况。情况1:panic没有被recover,执行逻辑将沿调用栈返回,直到goroutine退出。情况2:panic被recover捕获结束。
-
panic没有被捕获
下面的panic语句执行后,因为没有被捕获,所以沿着调用栈 main->fa->fb->fc 一路返回到到main中,然后程序直接退出了,main中的fd不会输出。因为在Go中,「如果一个goroutine panic了,而且这个goroutine里面没有捕获recover,那么整个进程就会挂掉」.
func main() {
go fa()
time.Sleep(time.Second)
fd()
}
func fa() {
fmt.Println("call fa")
fb()
}
func fb() {
fmt.Println("call fb")
fc()
}
func fc() {
fmt.Println("call fc")
panic("fc")
fmt.Println("call fc end")
}
func fd() {
fmt.Println("call fd")
}
上面的程序执行输出如下:
call fa
call fb
call fc
panic: fc
goroutine 6 [running]:
main.fc()
main.go:26 +0x95
main.fb()
main.go:21 +0x7a
main.fa()
main.go:16 +0x7a
created by main.main
main.go:9 +0x39
exit status 2
-
panic被捕获
下面程序中的panic异常被recover捕获,异常从ff返回后在f中被捕获处理了,main中的main end能够正常输出,并且程序不会挂掉。
func main() {
f()
fmt.Println("main end")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recover", r)
}
}()
ff()
fmt.Println("f end")
}
func ff() {
fmt.Println("a")
panic("foo")
fmt.Println("b")
}
上面的程序输出结果为:
a
recover foo
main end
需要注意的是,捕获代码recover逻辑需要放在defer语句中,否则函数将返回nil,看不到任何效果,因为defer语句在panic后也会被执行到。例如下面的程序,recover没有放在defer函数中,panic没有被捕获到。
func main() {
f()
fmt.Println("main end")
}
func f() {
if r := recover(); r != nil {
fmt.Println("recover", r)
}
ff()
fmt.Println("f end")
}
func ff() {
fmt.Println("a")
panic("foo")
fmt.Println("b")
}
上面的程序输出结果为:
a
panic: foo
goroutine 1 [running]:
main.ff()
main.go:23 +0x95
main.f()
main.go:16 +0x46
main.main()
main.go:6 +0x22
exit status 2
panic使用场景
在实际的工程项目中,对于错误处理通常都是采用error处理,使用panic是比较少的。看过Go源码的同学会注意到,源码中使用panic和throw是比较多的。那在什么场景下使用panic合适呢?
书中提到了两种适合采用panic的场景。场景1:使用在真正有异常的情况,例如程序员的错误。场景2:程序需要使用其他依赖,但是初始化失败。
对于场景1,书中列举了两个例子。例子1说的是net/http
包中的WriteHeader方法,它调用checkWriteHeaderCode函数时,该函数中用到panic函数。
func checkWriteHeaderCode(code int) {
if code < 100 || code > 999 {
panic(fmt.Sprintf("invalid WriteHeader code %v", code))
}
}
这里对http状态码的校验时候,如果不在[100,999]范围内直接panic,并且不捕获错误,出现这种情况,程序会直接挂掉。因为http协议对状态码有规范,如果传入的code值不在合法范围,说明程序员在传入的参数出现问题,这种人为存在的问题,直接panic让程序退出,显示暴露问题的做法比较合理。
例2来自database/sql
中的代码,在注册驱动Register函数中,如果driver为nil或者重复注册,这种也是人使用不当导致的问题,也直接panic。
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
在go-sql-driver/mysql
(Go中使用最多的MySQL驱动库)中,Register是通过init函数调用的,限制了error处理。综合这些因素,作者在设计的时候直接让程序panic处理。
// driver.go line 83
func init() {
sql.Register("mysql", &MySQLDriver{})
}
panic使用的场景是很少的,除了上面提到的程序人员导致的错误和依赖初始化失败情况外,其他采用error处理。