golang panic、recover函数以及defer语句

字数 1619阅读 138
panic(运行时恐慌)

demo:

fmt.Println("Enter function caller2.")
s1 := []int{0, 1, 2, 3, 4}
e5 := s1[5]
1 panic: runtime error: index of range
2
3 goroutine 1 [running]:
4 main.main()
5 /User/zheng/Golang_Puzzlers/demo.go:5 +0x3d
exit statu 2

这里的第一行是“panic: runtime error: index out of range”。其中的“runtime error”的含义是,这是一个runtime代码包中抛出的panic。在这个panic中,包含了一个runtime.Error接口类型的值。runtime.Error接口内嵌了error接口并做了一点点扩展,runtime包中有不少它的实现类型。

实际上,此详情中的“panic: ”右边的内容,正是这个panic包含的runtime.Error类型值的字符串表示形式。

此外,panic详情中一般还会包含与它的引发原因有关的goroutine的代码执行信息。正如前述详情中的“goroutine 1 [running]”,它表示有一个ID为1的goroutine在此panic被引发的时候正在运行。

注意,这里的ID其实并不重要,因为它只是Go语言运行时系统内部给予的一个goroutine编号,我们在程序中是无法获取和更改的。

我们再看下一行, “main.main()”表明了这个goroutine包装的go函数就是命令源码文件中的那个main函数,也就是说这里的goroutine正是主goroutine。再下面的一行,指出的就是这个goroutine中的哪一行
代码在此panic被引发时正在执行。

这包含了此行代码在其所属的源码文件中的行数,以及这个源码文件的绝对路径。这一行最后的+0x3d代表的是:此行代码相对于其所属函数的入口程序计数偏移量。不过,一般情况下它的用户并不大。

最后, “exit status 2”表明我的这个程序是以退出状态码2结束运行的。在大多数操作系统中,只要退出状态码不是0,都意味着程序运行的非正常结束。在Go语言中,因panic导致程序结束运行的退出状态码一般都会是2。

从panic被引发到程序终止运行的大致过程是什么?

 某个函数中的某行代码有意或无意地引发了一个panic。这时,初始的panic详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。

 这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。

 这里的外层函数指的就是go函数,对于主goroutine来说就是main函数。但是控制权也不会停留在那里,而是被Go语言运行时系统收回。

 随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。

怎样施加应对panic的保护措施,从而避免程序崩溃?

 Go语言的内建函数recover专用于恢复panic,或者说平息运行时恐慌。recover函数无需任何参数,并且会返回一个空接口类型的值。

 如果用法正确,这个值实际上就是即将恢复的panic包含的值。并且,如果这个panic是因我们调用panic函数而引发的,那么该值同时也会是我们此次调用panic函数时,传入的参数值副本。

package main

import (
  "fmt"
  "errors"
)

func main() {
    fmt.Println("Enter function main.")
    // 引发panic
    panic(errors.New("something wrong"))
    p := recover()
    fmt.Printf("panic: %s\n", p)
    fmt.Println("Exit function main.")
}

&emsp:在上面的函数值,先通过panic函数引发了一个panic,紧接着通过recover函数恢复这个panic。结果:程序依然会崩溃,因为panic一旦发生,控制权就会迅速地沿着调用栈的反方向传播。所以,在panic函数调用之后的代码,根本没有执行的机会。应该使用defer语句,defer语句就是用来延迟执行代码的,延迟到该语句所在的函数即将执行结束的那一刻。

package main

import (
  "fmt"
  "errors"
)

func main() {
    fmt.Println("Enter function main.")
    defer func(){
        fmt.Println("Enter defer function.")
        if p := recover(); p != nil {
          fmt.Println("panic: %s\n",p)
        }
    }
    // 引发panic
    panic(errors.New("something wrong"))
    fmt.Println("Exit function main.")
}

如果一个函数中有多条defer语句,那么几个defer函数调用的执行顺序是怎样的?

答: 在同一个函数中,defer函数调用的执行顺序与它们分别所属的defer语句的出现顺序(更严谨地说,是执行顺序)完全相反。

 当一个函数即将结束执行时,其中的写在最下边的defer函数调用会最先执行,其次是写在它上边、与它的距离最近的那个defer函数调用,以此类推,最上边的defer函数调用会最后一个执行。

 如果函数中有一条for语句,并且这条for语句中包含了一个defer语句,那么显然这条defer语句的执行次数,就取决于for语句的迭代次数。

 并且,同一条defer语句每被执行一次,其中的defer函数调用就会产生一次,并且,这些函数调用同样不会被立即执行。

 那么for语句中的多个defer函数的执行顺序:在defer语句每次执行的时候,Go语句会把它携带的defer函数及其参数值另行存储到一个队列中。这个队列与该defer语句所属的函数时对应的,并且,它是先进后出(FILO)的,相当于一个栈。在需要执行某个函数中的defer函数调用的时候,Go语言会先拿到对应的队列,然后从该队列中一个一个地取出defer函数及其参数值,并逐个执行调用。