分享

Go 语言系列30:异常处理

 菜籽爱编程 2022-04-27

上一期我们讲了 Go 中的错误处理,这一期讲讲 Go 中的 异常 处理。错误和异常是两个不同的概念,非常容易混淆。错误指的是可能出现问题的地方出现了问题;而异常指的是不应该出现问题的地方出现了问题。

panic

在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。

我们应该尽可能地使用错误,而不是使用 panicrecover 。只有当程序不能继续运行的时候,才应该使用 panicrecover 机制。

panic 有两个合理的用例:

  • 发生了一个不能恢复的错误,此时程序不能继续运行。一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic ,因为如果不能绑定端口,啥也做不了。
  • 发生了一个编程上的错误。假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic ,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

触发 panic

下面是内建函数 panic 的签名:

func panic(v interface{})

当程序终止时,会打印传入 panic 的参数。

package main

func main() {
 panic("runtime error")
}

运行上面的程序,会打印出传入 panic 函数的信息,并打印出堆栈跟踪:

panic: runtime error

goroutine 1 [running]:
main.main()
 (...)/main.go:4 +0x27


发生 panic 时的 defer

上面已经提到了,当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止。下面通过一个简单的例子看看是不是这样:

package main

import "fmt"

func myTest() {
 defer fmt.Println("defer in myTest")
 panic("runtime error")
}
func main() {
 defer fmt.Println("defer in main")
 myTest()
}

运行该程序后输出如下:

defer in myTest
defer in main
panic: runtime error

goroutine 1 [running]:
main.myTest()
 (...)/main.go:7 +0x73
main.main()
 (...)/main.go:11 +0x70


recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。下面是内建函数 recover 的签名:

func recover() interface{}

recover 必须在 defer 函数中才能生效,在其他作用域下,它是不工作的。在延迟函数内调用 recover ,可以取到 panic 的错误信息,并且停止 panic 续发事件,程序运行恢复正常。下面是网上找的一个例子:

package main

import "fmt"

func myTest(x int) {
 defer func() {
  // recover() 可以将捕获到的 panic 信息打印
  if err := recover(); err != nil {
   fmt.Println(err)
  }
 }()
 var array [10]int
 array[x] = 1
}
func main() {
 // 故意制造数组越界 触发 panic
 myTest(20)
 // 如果能执行到这句 说明 panic 被捕获了
 // 后续的程序能继续运行
 fmt.Println("everything is ok")
}

虽然该程序触发了 panic ,但由于我们使用了 recover() 捕获了 panic 异常,并输出 panic 信息,即使 panic 会导致整个程序退出,但在退出前,有 defer 延迟函数,还是得执行完 defer 。然后程序还会继续执行下去:

runtime error: index out of range [20] with length 10
everything is ok

这里要注意一点,只有在相同的协程中调用 recover 才管用, recover 不能恢复一个不同协程的 panic

package main

import (
 "fmt"
 "time"
)

func main() {
 // 这个 defer 并不会执行
 defer fmt.Println("in main")
 go func() {
  defer println("in goroutine")
  panic("")
 }()
 time.Sleep(time.Second)
}

运行后输出如下:

in goroutine
panic: 

goroutine 19 [running]:
main.main.func1()
 (...)/main.go:13 +0x7b
created by main.main
 (...)/main.go:11 +0x79

参考文献:

[1] Alan A. A. Donovan; Brian W. Kernighan, Go 程序设计语言, Translated by 李道兵, 高博, 庞向才, 金鑫鑫 and 林齐斌, 机械工业出版社, 2017.

👇周一至周五更新,期待你的关注👇

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多