GO语言通过error接口进行统一的错误处理,这些都是我们在编写代码时就可以预见并且返回的。但是面对一些我们不可知的,比如数组越界、除数为0、空指针引用,这些 Go 语言是怎么处理的呢?

panic

Go 语言没有像 PHP 那样引入异常的概念,也没有提供 try…catch 这样的语法对运行时异常进行捕获和处理,当代码运行时出错,而又没有在编码时显式返回错误时,Go 语言会抛出 panic,中文译作「运行时恐慌」,我们也可以将其看作 Go 语言版的异常。
除了 Go 语言底层抛出 panic,我们还可以在代码中显式抛出 panic,以便对错误和异常信息进行自定义,仍然以上篇教程除数为0的示例代码为例,我们可以这样显式返回 panic 中断代码执行:

  1. package main
  2. import "fmt"
  3. func main() {
  4. defer func() {
  5. fmt.Println("代码清理逻辑")
  6. }()
  7. var i = 1
  8. var j = 0
  9. if j == 0 {
  10. panic("除数不能为0!")
  11. }
  12. k := i / j
  13. fmt.Printf("%d / %d = %d\n", i, j, k)
  14. }

这样,当我们执行这段代码时,就会抛出 panic:
image.png
panic 函数支持的参数类型是interface{}

  1. func panic(v interface{})

所以可以传入任意类型的参数:

  1. panic(500) // 传入数字
  2. panic(errors.New("除数不能为0")) // 传入 error 类型

无论是 Go 语言底层抛出 panic,还是我们在代码中显式抛出 panic,处理机制都是一样的:当遇到 panic 时,Go 语言会中断当前协程中(main 函数)后续代码的执行,然后执行在中断代码之前定义的 defer 语句(按照先入后出的顺序),最后程序退出并输出 panic 错误信息,以及出现错误的堆栈跟踪信息,在这里就是:

  1. goroutine 1 [running]:
  2. main.main()
  3. /Users/sunqiang/Devlopment/golang/src/panic.go:13 +0x55
  4. exit status 2

第一行表示出问题的协程,第二行是问题代码所在的包和函数,第三行是问题代码的具体位置,最后一行则是程序的退出状态

recover

此外,我们还可以通过 recover() 函数对 panic 进行捕获和处理,从而避免程序崩溃然后直接退出,实现类似 PHP 中 try…catch…finally 的功能,由于执行到抛出 panic 的问题代码时,会中断后续其他代码的执行,所以,显然这个 panic 的捕获和其他代码的恢复执行需要放到 defer 语句中完成。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func divide() {
  6. defer func() {
  7. if err := recover(); err != nil {
  8. fmt.Printf("Runtime panic caught: %v\n", err)
  9. }
  10. }()
  11. var i = 1
  12. var j = 0
  13. k := i / j
  14. fmt.Printf("%d / %d = %d\n", i, j, k)
  15. }
  16. func main() {
  17. divide()
  18. fmt.Println("divide方法调用完毕,回到main函数")
  19. }

如果没有通过 recover() 函数捕获 panic 的话,程序会直接崩溃退出,并打印错误和堆栈信息:
image.png
而现在我们在 divide() 方法的 defer 语句中通过 recover() 函数捕获了 panic,并打印捕获到的错误信息,这个时候,程序会退出 divide() 方法而不是整个应用,继续执行 main() 函数中的后续代码,即恢复后续其他代码的执行:
image.png
这样一来,当程序运行过程中抛出 panic 时我们可以通过 recover() 函数对其进行捕获和处理,如果没有抛出则什么也不做,从而确保了代码的健壮性。