之前碰到的一个Golang面试题,关于defer和panic的执行顺序究竟是怎么样的,到底谁先执行谁后执行 ,来回顾验证学习一下;
Defer, panic 和 recover
Defer
defer语句会将函数推入到一个列表中。同时列表中的函数会在return语句执行后被调用。defer常常会被用来简化资源清理释放之类的操作。
defer语句的行为是明确可知的,此处有三条简单的规则:
-
函数参数值由defer语句调用时确定
比如下面这个例子,打印出来的变量i的值即是运行到defer语句时的值。在a函数执行return后,Defer后的函数调用,即Println,将会打印出 “0”。
1
2
3
4
5
6
|
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
|
-
deferred的函数将会在return语句之后按照先进后出的次序执行,即LIFO
下面这个函数的执行结果是"3210";
1
2
3
4
5
|
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
|
-
deferred函数还可以读取return返回值并改变其值
在下面的例子中,deferred函数中对返回值进行了自增操作,最终函数c的最终返回值是2.
1
2
3
4
5
6
|
func c() (i int) {
defer func() {
i++
}()
return 1
}
|
这使我们可以非常方便的修改异常的函数返回。
panic
panic是go的内置函数,它可以终止程序的正常执行流程并发出panic(类似其他语言的exception)。比如当函数F调用panic
,f的执行将被终止,然后defer的函数正常执行完后返回给调用者。对调用者而言,F的表现就像调用者直接调用了panic。这个流程会栈的调用次序不断向上抛出panic,直到返回到goroutine栈顶,此时,程序将会崩溃退出。panic可以通过直接调用panic
产生。同时也可能由运行时的错误所产生,例如数组越界访问。
recover
recover是go语言的内置函数,它的主要作用是可以从panic的重新夺回goroutine的控制权。Recover必须通过defer来运行。在正常的执行流程中,调用recover将会返回nil且没有什么其他的影响。但是如果当前的goroutine产生了panic,recover将会捕获到panic抛出的信息,同时恢复其正常的执行流程。
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package main
import "fmt"
func defer_call() {
defer func() {
fmt.Println("11111")
}()
defer func() {
fmt.Println("22222")
}()
defer func() {
if r := recover(); r != nil {
fmt.Println("Recover from r : ", r)
}
}()
defer func() {
fmt.Println("33333")
}()
fmt.Println("111 Helloworld")
panic("Panic 1!")
panic("Panic 2!")
fmt.Println("222 Helloworld")
}
func main() {
defer_call()
fmt.Println("333 Helloworld")
}
|
输出的结果是:
1
2
3
4
5
6
7
|
bomir@morn:~/Go/src/code/Interview100/DeferPanicOrder$ go run main.go
111 Helloworld
33333
Recover from r : Panic 1!
22222
11111
333 Helloworld
|
我们尝试用golang的gdb调试环境来具体分析下为什么会是这么个结果?
使用gdb调试
我们编译源代码使用 go build -gcflags "-l" main.go
,编译后使用gdb运行,
go里面的函数符号名称的命名规则是包名称.函数名称, 例如主函数的符号名称是main.main, 运行时中的newobject的符号名称是runtime.newobject.
首先给主函数下一个断点,给我们第一个panic("Panic 1!")
所在行下一个断点,然后运行:
panic源码解读
单步运行之后,找到了panic函数所对应的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
// runtime/panic.go 885行
// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
gp := getg()
if gp.m.curg != gp {
print("panic: ")
printany(e)
print("\n")
throw("panic on system stack")
}
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
}
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
// By calculating getcallerpc/getcallersp here, we avoid scanning the
// gopanic frame (stack scanning is slow...)
addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
for {
d := gp._defer // 获取当前协程defer链表的头结点
if d == nil {
break // 当前协程的defer都被执行后,defer链表为空,
// 此时退出循环
}
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. An earlier panic will not continue running, but we will make sure below that an
// earlier Goexit does continue running.
if d.started {
// 发生panic之后, 在defer中又遇到panic(), 则会进入这个模块
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
if !d.openDefer {
// For open-coded defers, we need to process the
// defer again, in case there are any other defers
// to call in the frame (not including the defer
// call that caused the panic).
d.fn = nil
gp._defer = d.link
// defer已经被执行过, 则释放这个defer, 继续for循环
freedefer(d)
continue
}
}
// Mark defer as started, but keep on list, so that traceback
// can find and update the defer's argument frame if stack growth
// or a garbage collection happens before reflectcall starts executing d.fn.
d.started = true
// Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
done := true
if d.openDefer {
done = runOpenDeferFrame(gp, d)
if done && !d._panic.recovered {
addOneOpenDeferFrame(gp, 0, nil)
}
} else {
p.argp = unsafe.Pointer(getargp(0))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
}
p.argp = nil
// reflectcall did not panic. Remove d.
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC()
pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
if done {
d.fn = nil
gp._defer = d.link // 从defer链中移除刚刚执行过的defer
freedefer(d) // 释放刚刚执行过的defer
}
if p.recovered { // // defer()中遇到recover后进入这个代码块
gp._panic = p.link
if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
// A normal recover would bypass/abort the Goexit. Instead,
// we return to the processing loop of the Goexit.
gp.sigcode0 = uintptr(gp._panic.sp)
gp.sigcode1 = uintptr(gp._panic.pc)
// 跳转到recover()处,继续往下执行
mcall(recovery)
throw("bypassed recovery failed") // mcall should not return
}
atomic.Xadd(&runningPanicDefers, -1)
if done {
// Remove any remaining non-started, open-coded
// defer entries after a recover, since the
// corresponding defers will be executed normally
// (inline). Any such entry will become stale once
// we run the corresponding defers inline and exit
// the associated stack frame.
d := gp._defer
var prev *_defer
for d != nil {
if d.openDefer {
if d.started {
// This defer is started but we
// are in the middle of a
// defer-panic-recover inside of
// it, so don't remove it or any
// further defer entries
break
}
if prev == nil {
gp._defer = d.link
} else {
prev.link = d.link
}
newd := d.link
freedefer(d)
d = newd
} else {
prev = d
d = d.link
}
}
}
gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig = 0
}
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
// ran out of deferred calls - old-school panic now
// Because it is unsafe to call arbitrary user code after freezing
// the world, we call preprintpanics to invoke all necessary Error
// and String methods to prepare the panic strings before startpanic.
preprintpanics(gp._panic) // 输出panic信息
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
|
结论
上面代码虽然有些没有看懂,但是其执行流程还是比较清楚,从代码上来看,**协程遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中,遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。从执行顺序上来看,实际上是按照先进后出的顺序执行defer。**这个时候应该会理解上面的面试题答案为什么是那样了。