1.什么是panic、recover、defer?
panic:运行时恐慌,在程序运行时抛出的异常。
recover:专用于恢复panic,平息运行时恐慌。recover()函数无需任何参数,会返回一个空接口类型的值,这个值是即将恢复的panic包含的值。一般与defer使用。
defer:defer语句是被用来延迟执行代码的,延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么。
2.从panic被引发到程序终止运行的大致过程是什么?
产生运行时异常,可以是没意料到的,比如数组越界等,也可以手动使用panic()函数。比如下述代码中因为数组越界产生了panic:
import (
"fmt"
)
func main() {
fmt.Println("enter main")
caller1() (1)
fmt.Println("exit main")
}
func caller1(){
fmt.Println("enter caller1")
caller2() (2)
fmt.Println("exit caller1")
}
func caller2(){
fmt.Println("enter caller2")
s1:=[]int{0,1,2,3}
_=s1[5] (3)
fmt.Println("exit caller2")
}
运行结果:
caller2()函数无意间引发一个panic,初始的panic详情会被建立起来,图中位置(3);
该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,图中位置(2);
控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处,图中位置(1)。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里,这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数;
控制权不会停留在那里,而是被 Go 语言运行时系统收回;
程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。
3.recover()如何恢复panic()?
围绕问题2中示例,代码如下:
import (
"fmt"
)
func main() {
fmt.Println("enter main")
caller1()
fmt.Println("exit main")
}
func caller1(){
fmt.Println("enter caller1")
caller2()
fmt.Println("exit caller1")
}
func caller2(){
fmt.Println("enter caller2")
defer func() {
fmt.Println("enter defer")
if p:=recover();p!=nil{
fmt.Printf("panic:%s\n",p)
}
fmt.Println("exit defer")
}()
s1:=[]int{0,1,2,3}
_=s1[5]
fmt.Println("exit caller2")
}
运行结果如下:
一定要注意,我们要尽量把defer语句写在函数体的开始处,因为在引发 panic 的语句之后的所有语句,都不会有任何执行机会。这样的话,defer函数中的recover函数调用才会拦截,并恢复defer语句所属的函数,及其调用的代码中发生的所有 panic。
4.一个函数中有多条defer语句,那么那几个defer函数调用的执行顺序是怎样的?
在同一个函数中,defer函数调用的执行顺序与它们分别所属的defer语句的执行顺序完全相反。当一个函数即将结束执行时,其中写在最下边的defer函数调用会最先执行,其次是写在它上边、与它的距离最近的那个defer函数调用,以此类推,最上边的defer函数调用会最后一个执行。
在defer语句每次执行的时候,Go 语言会把它携带的defer函数及其参数值另行存储到一个链表中。这个链表与该defer语句所属的函数是对应的,并且,它是先进后出(FILO)的,相当于一个栈。在需要执行某个函数中的defer函数调用的时候,Go 语言会先拿到对应的链表,然后从该链表中一个一个地取出defer函数及其参数值,并逐个执行调用。
参考资料:Go语言核心36讲