网上有一道关于panic和defer的判断题:“【中级】当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点,该携程结束,然后终止其他所有携程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数()”
这道题的考察点很多,要理解整个panic和defer的实现机制才能把这道题完全答对。首先把这段话切分成几个点:
- 显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数(引用空指针、下标越界的情况这里暂时不写)
- 调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点
- 该协程结束,然后终止其他所有协程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数
1)先触发panic函数还是先调用延迟函数
如下例所示,example.go
package main
import (
"fmt"
)
func defunc1() {
fmt.Printf("defunc1 is called\n")
}
func test() {
defer defunc1()
panic("Not working!!")
}
func main() {
test()
}
输出:
[root@dev example]# go run example.go
defunc1 is called
panic: Not working!!
goroutine 1 [running]:
main.test()
/home/go-test/src/example/example.go:13 +0x5f
main.main()
/home/go-test/src/example/example.go:17 +0x20
exit status 2
从输出来看defunc1先打印输出,而panic后打印输出,貌似defunc延迟函数先调用。可是,如果不先调用panic,那正常的执行流程又是怎么被中断的呢,这是一个矛盾。
下面我们来看一下这段代码的汇编代码,为了简化汇编代码,稍作改动把对fmt的引用及调用换成对全局变量的赋值:
package main
var g int
func defunc1() {
g = 13
}
func test() {
defer defunc1()
panic("Not working!!")
}
func main() {
g = 17
test()
}
通过命令“GOOS=linux GOARCH=386 go tool compile -S example.go > example.S”得到汇编代码,其中test函数的代码片段如下:
"".test STEXT size=96 args=0x0 locals=0x28
26 0x0000 00000 (example.go:9) TEXT "".test(SB), ABIInternal, $40-0
27 0x0000 00000 (example.go:9) MOVL TLS, CX
28 0x0007 00007 (example.go:9) MOVL (CX)(TLS*2), CX
29 0x000d 00013 (example.go:9) CMPL SP, 8(CX)
30 0x0010 00016 (example.go:9) JLS 89
31 0x0012 00018 (example.go:9) SUBL $40, SP
32 0x0015 00021 (example.go:9) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
33 0x0015 00021 (example.go:9) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
34 0x0015 00021 (example.go:9) FUNCDATA $2, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
35 0x0015 00021 (example.go:10) PCDATA $0, $0
36 0x0015 00021 (example.go:10) PCDATA $1, $0
37 0x0015 00021 (example.go:10) MOVL $0, ""..autotmp_0+8(SP)
38 0x001d 00029 (example.go:10) PCDATA $0, $1
39 0x001d 00029 (example.go:10) LEAL "".defunc1·f(SB), AX
40 0x0023 00035 (example.go:10) PCDATA $0, $0
41 0x0023 00035 (example.go:10) MOVL AX, ""..autotmp_0+24(SP)
42 0x0027 00039 (example.go:10) PCDATA $0, $1
43 0x0027 00039 (example.go:10) LEAL ""..autotmp_0+8(SP), AX
44 0x002b 00043 (example.go:10) PCDATA $0, $0
45 0x002b 00043 (example.go:10) MOVL AX, (SP)
46 0x002e 00046 (example.go:10) CALL runtime.deferprocStack(SB) //延迟函数入先进后出队列
47 0x0033 00051 (example.go:10) TESTL AX, AX
48 0x0035 00053 (example.go:10) JNE 79
49 0x0037 00055 (example.go:11) PCDATA $0, $1
50 0x0037 00055 (example.go:11) LEAL type.string(SB), AX
51 0x003d 00061 (example.go:11) PCDATA $0, $0
52 0x003d 00061 (example.go:11) MOVL AX, (SP)
53 0x0040 00064 (example.go:11) PCDATA $0, $1
54 0x0040 00064 (example.go:11) LEAL ""..stmp_0(SB), AX
55 0x0046 00070 (example.go:11) PCDATA $0, $0
56 0x0046 00070 (example.go:11) MOVL AX, 4(SP)
57 0x004a 00074 (example.go:11) CALL runtime.gopanic(SB) // panic编译成对gopanic的调用
58 0x004f 00079 (example.go:10) XCHGL AX, AX
59 0x0050 00080 (example.go:10) CALL runtime.deferreturn(SB) //只要有defer函数,编译器就会在函数返回前插入对延迟函数队列的执行,直到队列为空
60 0x0055 00085 (example.go:10) ADDL $40, SP
61 0x0058 00088 (example.go:10) RET //函数返回
62 0x0059 00089 (example.go:10) NOP
63 0x0059 00089 (example.go:9) PCDATA $1, $-1
64 0x0059 00089 (example.go:9) PCDATA $0, $-1
65 0x0059 00089 (example.go:9) CALL runtime.morestack_noctxt(SB)
66 0x005e 00094 (example.go:9) JMP 0
这段汇编里有3个重要函数,都在runtime.go源文件里。
- runtime.deferprocStack: 对应的是(example.go:10)defer defunc1这一句,把延迟函数加入到该协程对应的一个延迟函数链表,每个协程各自维护一个延迟函数链表。
func deferprocStack(d *_defer) { gp := getg() //获取当前协程,不同的协程维护不同的延迟函数链表 ... //把d置成新的链表表头 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) return0() }
- runtime.gopanic,对应的是(example.go:11)panic("Not working!!")这一句,如果是系统级别异常(如panic during malloc)则直接退出进程不会执行任何延迟函数。如果是非系统级别异常,则按先进后出顺序调用runtime.deferreturn来逐个执行本协程的延迟函数链表,如果某个被执行的延迟函数包含有对recover的调用,则恢复正常。如果都没有对recover的调用,则打印输出panic的消息如这里的"Not working!!",然后打印stack trace,最后调用exit(2)结束进程。
func gopanic(e interface{}) { gp := getg() //获取当前协程 ... for { d := gp._defer //获取当前协程的延迟函数链表头 ... //调用延迟函数 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) ... gp._defer = d.link //移动到延迟函数链表的下一个节点 freedefer(d) //释放链表节点d if p.recovered { ... mcall(recovery) //恢复协程 ... } } preprintpanics(gp._panic) //打印panic消息 fatalpanic(gp._panic) // 打印stack trace,退出进程 *(*int)(nil) = 0 // not reached }
func fatalpanic(msgs *_panic) { ... systemstack(func() { exit(2) //退出进程 }) *(*int)(nil) = 0 // not reached }
- runtime.deferreturn,这个是编译器追加的,如果已经有panic发生,则在第2步就已经调用过deferreturn了,延迟函数链表已经为空。如果没有panic发生,则执行延迟函数链表里的延迟函数。
由此可见,在调用顺序上是先调用panic对应的gopanic函数, 由gopanic函数去执行延迟函数,所以“先触发panic函数的执行,然后调用延迟函数”是正确的。
2)子函数是否会把panic传递给父函数
同一个协程的所有延迟函数会维护在同一个链表当中,当子函数触发panic即调用runtime.gopanic函数时,它会执行完链表里的所有延迟函数,直到遇到某个延迟函数包含有对recover的调用,否则直接退出进程,注意是退出进程而不是协程。因此,panic是不会在父子函数之间传递的。
所以“调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点”这一句是错误的。
3)触发panic的协程是如何停止所有协程的
gopanic函数调用fatalpanic,再由fatalpanic调用systemstack,systemstack最终调用exit(2),退出“进程”,而不是仅仅退出“协程”,其它协程的延迟函数不会被执行。这里的exit在不同的操作系统上对应的函数不同,例如在windows上它对应的是
src/runtime/os_windows.go +556调用windows操作系统函数_ExitProcess(Ends the calling process and all its threads.)退出进程。
func exit(code int32) {
atomic.Store(&exiting, 1)
stdcall1(_ExitProcess, uintptr(code))
}
下面的代码可以验证其它协程的延迟函数是否会执行:
package main
import (
"fmt"
"time"
)
func defunc1() {
fmt.Printf("defunc1 is called\n")
}
func defunc2() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic that is defunc2: ", r)
}
fmt.Printf("defunc2 is called\n")
}
func test() {
defer defunc1()
panic("Not working!!")
}
func main() {
defer defunc2()
go func() {
test()
}()
time.Sleep(3 * time.Second)
fmt.Printf("main exit -----------")
}
输出:
defunc1 is called
panic: Not working!!
goroutine 6 [running]:
main.test()
/home/zhoupeng/go-test/src/example/m.go:22 +0x5f
main.main.func1()
/home/zhoupeng/go-test/src/example/m.go:29 +0x20
created by main.main
/home/zhoupeng/go-test/src/example/m.go:28 +0x6d
exit status 2
可见,如果另起一个协程来调用test(),则当test()触发panic时,main函数里的延迟函数不会被执行。如果上面的代码对test()的调用改成放在main函数所在的协程:
func main() {
defer defunc2()
//go func() {
test()
//}()
time.Sleep(3 * time.Second)
fmt.Printf("main exit -----------")
}
则输出:
defunc1 is called
Recovered from panic that is defunc2: Not working!!
defunc2 is called
所以“该协程结束,然后终止其他所有协程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数”这句是错误的。
4)panic之后的defer函数是否会被执行
题目里并没有问这一点,但这也是panic/defer机制的一部分。例如在panic之后再增加两个defer:defer defunc3()和defer defunc4()
func test() {
defer defunc1()
panic("Not working!!")
defer defunc3()
}
func main() {
defer defunc2()
test()
defer defunc4()
}
从汇编代码,Go的源码和输出结果来看,defer defunc3()由于和panic处在同一个test()函数并且在panic之后,因此会被编译器在编译过程中直接忽略掉;而defer defunc4()会被编译成runtime.deferprocStack,但是在panic触发后进程直接退出,defunc4的runtime.deferprocStack还没有被调用,还没有进入延迟队列,所以不会被执行。
5) panic触发之后defer函数里再次触发新的panic,之前的panic怎么处理
例如在下面的代码中,panic1触发之后执行最后一个defer再触发panic2, panic2触发后再执行倒数第二个defer进而触发panic3,而Go的机制是如果当前defer函数是由之前的panic触发的并且当前defer函数会触发新的panic,则前一个panic停止执行。也就是说下面代码中当panic2触发时,panic1的执行过程将停止,当panic3触发时panic2的执行过程将停止,所以最终能被recover捕获到的只有panic3。
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
panic("panic3")
}()
defer func() {
panic("panic2")
}()
panic("panic1")
}
输出:
panic3
这部分源码在“src/runtime/panic.go”的gopanic函数里。它的注释写的比较清楚,当我们因为触发了一个新的panic而再次运行到这里时,如果这个defer是由之前的panic调用起来的,则前一个panic停止执行。
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
注:以上测试的Go版本是1.13.8