这是 A Journey With Go翻译的第二篇文章,大家感兴趣可以去看看原文。我是带着学习的目的去翻译,既能学习英文,又能学习技术,一举二得。
所以翻译的不好是正常的,有疑问可以交流。
正文
在go中如果没有恰当的处理error,会造成panic,比如非法的内存操作。 如果这个错误是预料之外的,也没有其它的处理方法,也可以由开发者主动panic。
了解recover或终止是如何处理的,有助于理解一个会发生panic的程序带来的后果
多层函数调用
关于panic和recover的经典的例子有被记录到文档里,收录在go的博客中“Defer, Panic, and Recover.” 让我们关注另一个例子,其中panic涉及多个defer函数的帧时
package main
func main() {
defer println("defer 1")
level1()
}
func level1() {
defer println("defer 3")
defer func() {
if err := recover(); err != nil {
println("revoring in progress")
}
}()
defer println("defer 2")
level2()
}
func level2() {
defer println("defer func 4")
panic("foo")
}
这段代码的输出为:
defer func 4
defer 2
revoring in progress
defer 3
defer 1
这段代码有三个函数组成,他们是链式调用。一旦代码运行到leve2函数中,将会触发panic,然后执行defer
这段红色框中的代码不会recover这个panic,然后会返回到父调用者也就是level1中,依次执行defer函数
值得一提的是,defer的执行顺序是LIFO,后进先出,类似于stack。更多关于defer的内容可以看另一篇文章“Go: How Does defer Statement Work?”
由于在level1中将会recover这个panic,go需要一个方法追踪记录它并恢复程序的执行。
为此,每个goroutine有一个特殊的属性指向一个代表该panic的对象。
当一个panic发生时,这个对象将会被创建在执行defer之前。然后recover这个panic的函数实际上仅仅返回这个panic对象的信息,同时标示被恢复了
一旦panic被认为已恢复,go将会继续当前的工作,然而 由于运行时在多个defer函数中,它不知道从哪继续执行。鉴于这个原因,
Go保存当前程序计数器和当前帧的堆栈指针,以便panic发生后恢复该函数
我们可以使用objdump,检查当前程序计数器
(e.g. objdump -D my-binary | grep 105acef
):
这个指令指向untime.deferreturn,被编译器插入到每个有defer的函数的最后。在上面的例子中,直到recover前大部分的已经运行了,
因此剩下的将会运行在return到调用者之前。
WaitGroup
理解这个工作流向我们展示了defer功能的重要性,以及它是如何发挥作用的
举个例子,当使用一组goroutine时,
在延迟函数中使用defer对WaitGroup对象的调用可以防止死锁
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer func() {
if err := recover(); err != nil {
println(err.(string))
}
}()
p()
wg.Done()
}()
wg.Wait()
}
func p() {
panic("foo")
}
这个程序会导致死锁,因为wg.Done从来不会被调用。使用 defer可以解决这个问题。
package main
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer func() {
if err := recover(); err != nil {
println(err.(string))
}
}()
defer wg.Done()
p()
}()
wg.Wait()
}
func p() {
panic("foo")
}
Goexit
值得注意的是,这个函数runtime.Goexit使用完全相同的工作流。它实际上创建了一个带有特殊标志的panic对象,以区别于真正的panic。
此标志允许运行时跳过recover并正确退出,而不是停止程序的执行。