vlambda博客
学习文章列表

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处理。