Go语言中常见100问题-#48 panic
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{}) }
对于场景2,如果我们的程序依赖需要其他依赖,但是依赖初始化失败,我们的程序是没法工作的。例如,我们提供的一个创建账户的服务,服务某个处理过程中需要对用户提供的邮箱地址进行验证,我们决定采用正则表达式对email address进行校验。
在Go中,regexp包提供了两个创建正则的函数,它们是Compile和MustCompile。前者返回的是regexp.Regexp和一个error. 然而后者只返回一个regexp.Regexp,但是出现错误,会直接panic,这是一种强依赖,的确,如果compile失败,将不能够验证任何输入的邮箱地址。因此,相比返回错误,采用MustCompile和panic要更合适。
panic使用的场景是很少的,除了上面提到的程序人员导致的错误和依赖初始化失败情况外,其他采用error处理。