defer
Go 语言的 defer
会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。
常用的一种方式为:
1
2
3
4
5
6
7
8
9
10
|
func createPost(db *gorm.DB) error {
tx := db.Begin()
defer tx.Rollback()
if err := tx.Create(&Post{Author: ""}).Error; err != nil {
return err
}
return tx.Commit().Error
}
|
如果tx.Commit()执行失败,则函数退出时会默认执行tx.Rollback(),但是如果执行成功,也会执行tx.Rollback(),但是不会影响已经提交的事务。
需要思考两个问题:
- 多个defer函数的调用顺序:
- 后调用的
defer
函数会被追加到 Goroutine _defer
链表的最前面;
- 运行
runtime._defer
时是从前到后依次执行;
defer
关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果:调用 runtime.deferproc
函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;
defer函数执行顺序
假设多次调用defer
1
2
3
4
5
|
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
|
其输出为:
所以defer的调用顺序为先进后出的方式(类似于堆栈,实际上是链表,后加入的会插入到链表的首部
),defer函数只会在当前函数退出前被调用。
在看下一段代码的输出
1
2
3
4
5
6
7
8
9
10
|
func testDeferValue() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
func main() {
testDeferValue()
}
|
其输出为:
而我们预期的输出是1s,问题出在哪?
Go 语言中所有的函数调用都是传值的,虽然 defer
是关键字,但是也继承了这个特性。调用 defer
关键字会立刻拷贝函数中引用的外部参数,所以 time.Since(startedAt)
的结果不是在 main
函数退出之前计算的,而是在 defer
关键字调用时计算的,最终导致上述代码输出 0s。
1
2
3
4
5
6
7
8
9
|
func test() (a int) {
a = 5
defer println("direct", a)
defer func() {
println("func", a)
}()
a = 10
return a
}
|
上面这个函数的输出为:
虽然调用 defer
关键字时也使用值传递,但是因为拷贝的是函数指针,所以使用匿名函数能解决上述问题。
defer数据结构
defer的数据结构如下
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
|
type _defer struct {
siz int32 // includes both arguments and results
started bool
heap bool
// openDefer indicates that this _defer is for a frame with open-coded
// defers. We have only one defer record for the entire frame (which may
// currently have 0, 1, or more defers active).
openDefer bool
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn *funcval // can be nil for open-coded defers
_panic *_panic // panic that is running defer
link *_defer
// If openDefer is true, the fields below record values about the stack
// frame and associated function that has the open-coded defer(s). sp
// above will be the sp for the frame, and pc will be address of the
// deferreturn call in the function.
fd unsafe.Pointer // funcdata for the function associated with the frame
varp uintptr // value of varp for the stack frame
// framepc is the current pc associated with the stack frame. Together,
// with sp above (which is the sp associated with the stack frame),
// framepc/sp can be used as pc/sp pair to continue a stack trace via
// gentraceback().
framepc uintptr
}
|
defer是一个链表,通过link将defer连接到一起。
siz
是参数和结果的内存大小;
sp
和 pc
分别代表栈指针和调用方的程序计数器;
fn
是 defer
关键字中传入的函数;
_panic
是触发延迟调用的结构体,可能为空;
openDefer
表示当前 defer
是否经过开放编码的优化;
执行
中间代码生成阶段会负责处理程序中的defer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
case ODEFER:
if Debug_defer > 0 {
var defertype string
if s.hasOpenDefers {
defertype = "open-coded"
} else if n.Esc == EscNever {
defertype = "stack-allocated"
} else {
defertype = "heap-allocated"
}
Warnl(n.Pos, "%s defer", defertype)
}
if s.hasOpenDefers {
s.openDeferRecord(n.Left) // 开放编码
} else {
d := callDefer // 堆分配
if n.Esc == EscNever {
d = callDeferStack // 栈分配
}
s.callResult(n.Left, d)
}
|
堆上分配
根据 cmd/compile/internal/gc.state.stmt
方法对 defer
的处理我们可以看出,堆上分配的 runtime._defer
结构体是默认的兜底方案,当该方案被启用时,编译器会调用 cmd/compile/internal/gc.state.callResult
和 cmd/compile/internal/gc.state.call
,这表示 defer
在编译器看来也是函数调用。
cmd/compile/internal/gc.state.call
会负责为所有函数和方法调用生成中间代码,它的工作包括以下内容:
- 获取需要执行的函数名、闭包指针、代码指针和函数调用的接收方;
- 获取栈地址并将函数或者方法的参数写入栈中;
- 使用
cmd/compile/internal/gc.state.newValue1A
以及相关函数生成函数调用的中间代码;
- 如果当前调用的函数是
defer
,那么会单独生成相关的结束代码块;
- 获取函数的返回值地址并结束当前调用;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
func (s *state) call(n *Node, k callKind, returnResultAddr bool) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上初始化 defer 结构体
...
} else {
...
switch {
case k == callDefer:
aux := ssa.StaticAuxCall(deferproc, ACArgs, ACResults)
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, aux, s.mem())
...
}
call.AuxInt = stksize
}
s.vars[&memVar] = call
...
}
|
defer
关键字在运行期间会调用 runtime.deferproc
,这个函数接收了参数的大小和闭包所在的地址两个参数。
编译器不仅将 defer
关键字都转换成 runtime.deferproc
函数,它还会通过以下三个步骤为所有调用 defer
的函数末尾插入 runtime.deferreturn
的函数调用:
1
2
3
4
5
6
7
|
func (s *state) exit() *ssa.Block {
if s.hasdefer {
...
s.rtcall(Deferreturn, true, nil)
}
...
}
|
当运行时将 runtime._defer
分配到堆上时,Go 语言的编译器不仅将 defer
转换成了 runtime.deferproc
,还在所有调用 defer
的函数结尾插入了 runtime.deferreturn
。上述两个运行时函数是 defer
关键字运行时机制的入口,它们分别承担了不同的工作:
创建延迟调用
runtime.deferproc
会为 defer
创建一个新的 runtime._defer
结构体、设置它的函数指针 fn
、程序计数器 pc
和栈指针 sp
并将相关的参数拷贝到相邻的内存空间中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func deferproc(siz int32, fn *funcval) {
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
|
最后调用的 runtime.return0
是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn
的递归调用。
runtime.deferproc
中 runtime.newdefer
的作用是想尽办法获得 runtime._defer
结构体,这里包含三种路径:
- 从调度器的延迟调用缓存池
sched.deferpool
中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中;
- 从 Goroutine 的延迟调用缓存池
pp.deferpool
中取出结构体;
- 通过
runtime.mallocgc
在堆上创建一个新的结构体;
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
|
func newdefer(siz int32) *_defer {
var d *_defer
sc := deferclass(uintptr(siz))
gp := getg()
if sc < uintptr(len(p{}.deferpool)) {
pp := gp.m.p.ptr()
if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
}
if n := len(pp.deferpool[sc]); n > 0 {
d = pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
}
}
if d == nil {
total := roundupsize(totaldefersize(uintptr(siz)))
d = (*_defer)(mallocgc(total, deferType, true))
}
d.siz = siz
d.link = gp._defer
gp._defer = d
return d
}
|
无论使用哪种方式,只要获取到 runtime._defer
结构体,它都会被追加到所在 Goroutine _defer
链表的最前面。
defer
关键字的插入顺序是从后向前的,而 defer
关键字执行是从前向后的,这也是为什么后调用的 defer
会优先执行。
执行延迟调用
runtime.deferreturn
会从 Goroutine 的 _defer
链表中取出最前面的 runtime._defer
并调用 runtime.jmpdefer
传入需要执行的函数和参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
if d == nil {
return
}
sp := getcallersp()
...
switch d.siz {
case 0:
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
gp._defer = d.link
freedefer(d)
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
|
runtime.jmpdefer
是一个用汇编语言实现的运行时函数,它的主要工作是跳转到 defer
所在的代码段并在执行结束之后跳转回 runtime.deferreturn
。
栈上分配
在默认情况下,我们可以看到 Go 语言中 runtime._defer
结构体都会在堆上分配,如果我们能够将部分结构体分配到栈上就可以节约内存分配带来的额外开销。当该关键字在函数体中最多执行一次时,编译期间的 cmd/compile/internal/gc.state.call
会将结构体分配到栈上并调用 runtime.deferprocStack
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func (s *state) call(n *Node, k callKind) *ssa.Value {
...
var call *ssa.Value
if k == callDeferStack {
// 在栈上创建 _defer 结构体
t := deferstruct(stksize)
...
ACArgs = append(ACArgs, ssa.Param{Type: types.Types[TUINTPTR], Offset: int32(Ctxt.FixedFrameSize())})
aux := ssa.StaticAuxCall(deferprocStack, ACArgs, ACResults) // 调用 deferprocStack
arg0 := s.constOffPtrSP(types.Types[TUINTPTR], Ctxt.FixedFrameSize())
s.store(types.Types[TUINTPTR], arg0, addr)
call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, aux, s.mem())
call.AuxInt = stksize
} else {
...
}
s.vars[&memVar] = call
...
}
|
因为在编译期间我们已经创建了 runtime._defer
结构体,所以在运行期间 runtime.deferprocStack
只需要设置一些未在编译期间初始化的字段,就可以将栈上的 runtime._defer
追加到函数的链表上:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
func deferprocStack(d *_defer) {
gp := getg()
d.started = false
d.heap = false // 栈上分配的 _defer
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
}
|
除了分配位置的不同,栈上分配和堆上分配的 runtime._defer
并没有本质的不同,而该方法可以适用于绝大多数的场景,与堆上分配的 runtime._defer
相比,该方法可以将 defer
关键字的额外开销降低 ~30%。
开放编码
Go 语言在 1.14 中通过开放编码(Open Coded)实现 defer
关键字,该设计使用代码内联优化 defer
关键的额外开销并引入函数数据 funcdata
管理 panic
的调用,该优化可以将 defer
的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右。
然而开放编码作为一种优化 defer
关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:
- 函数的
defer
数量少于或者等于 8 个;
- 函数的
defer
关键字不能在循环中执行;
- 函数的
return
语句与 defer
语句的乘积小于或者等于 15 个;
启用优化
Go 语言会在编译期间就确定是否启用开放编码,在编译器生成中间代码之前,我们会使用 cmd/compile/internal/gc.walkstmt
修改已经生成的抽象语法树,设置函数体上的 OpenCodedDeferDisallowed
属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const maxOpenDefers = 8
func walkstmt(n *Node) *Node {
switch n.Op {
case ODEFER:
Curfn.Func.SetHasDefer(true)
Curfn.Func.numDefers++
if Curfn.Func.numDefers > maxOpenDefers {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
if n.Esc != EscNever {
Curfn.Func.SetOpenCodedDeferDisallowed(true)
}
fallthrough
...
}
}
|
就像我们上面提到的,如果函数中 defer
关键字的数量多于 8 个或者 defer
关键字处于 for
循环中,那么我们在这里都会禁用开放编码优化,使用上两节提到的方法处理 defer
。
在 SSA 中间代码生成阶段的 cmd/compile/internal/gc.buildssa
中,我们也能够看到启用开放编码优化的其他条件,也就是返回语句的数量与 defer
数量的乘积需要小于 15:
1
2
3
4
5
6
7
8
9
10
|
func buildssa(fn *Node, worker int) *ssa.Func {
...
s.hasOpenDefers = s.hasdefer && !s.curfn.Func.OpenCodedDeferDisallowed()
...
if s.hasOpenDefers &&
s.curfn.Func.numReturns*s.curfn.Func.numDefers > 15 {
s.hasOpenDefers = false
}
...
}
|
中间代码生成的这两个步骤会决定当前函数是否应该使用开放编码优化 defer
关键字,一旦确定使用开放编码,就会在编译期间初始化延迟比特和延迟记录。
延迟记录
延迟比特和延迟记录是使用开放编码实现 defer
的两个最重要结构,一旦决定使用开放编码,cmd/compile/internal/gc.buildssa
会在编译期间在栈上初始化大小为 8 个比特的 deferBits
变量:
1
2
3
4
5
6
7
8
9
10
11
12
|
func buildssa(fn *Node, worker int) *ssa.Func {
...
if s.hasOpenDefers {
deferBitsTemp := tempAt(src.NoXPos, s.curfn, types.Types[TUINT8]) // 初始化延迟比特
s.deferBitsTemp = deferBitsTemp
startDeferBits := s.entryNewValue0(ssa.OpConst8, types.Types[TUINT8])
s.vars[&deferBitsVar] = startDeferBits
s.deferBitsAddr = s.addr(deferBitsTemp)
s.store(types.Types[TUINT8], s.deferBitsAddr, startDeferBits)
s.vars[&memVar] = s.newValue1Apos(ssa.OpVarLive, types.TypeMem, deferBitsTemp, s.mem(), false)
}
}
|
延迟比特中的每一个比特位都表示该位对应的 defer
关键字是否需要被执行。
因为不是函数中所有的 defer
语句都会在函数返回前执行,如下所示的代码只会在 if
语句的条件为真时,其中的 defer
语句才会在结尾被执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
deferBits := 0 // 初始化 deferBits
_f1, _a1 := f1, a1 // 保存函数以及参数
deferBits |= 1 << 0 // 将 deferBits 最后一位置位 1
if condition {
_f2, _a2 := f2, a2 // 保存函数以及参数
deferBits |= 1 << 1 // 将 deferBits 倒数第二位置位 1
}
exit:
if deferBits & 1 << 1 != 0 {
deferBits &^= 1 << 1
_f2(a2)
}
if deferBits & 1 << 0 != 0 {
deferBits &^= 1 << 0
_f1(a1)
}
|
延迟比特的作用就是标记哪些 defer
关键字在函数中被执行,这样在函数返回时可以根据对应 deferBits
的内容确定执行的函数,而正是因为 deferBits
的大小仅为 8 比特,所以该优化的启用条件为函数中的 defer
关键字少于 8 个。
上述伪代码展示了开放编码的实现原理,但是仍然缺少了一些细节,例如:传入 defer
关键字的函数和参数都会存储在如下所示的 cmd/compile/internal/gc.openDeferInfo
结构体中:
1
2
3
4
5
6
7
8
9
|
type openDeferInfo struct {
n *Node
closure *ssa.Value
closureNode *Node
rcvr *ssa.Value
rcvrNode *Node
argVals []*ssa.Value
argNodes []*Node
}
|
当编译器在调用 cmd/compile/internal/gc.buildssa
构建中间代码时会通过 cmd/compile/internal/gc.state.openDeferRecord
方法在栈上构建结构体,该结构体的 closure
中存储着调用的函数,rcvr
中存储着方法的接收者,而最后的 argVals
中存储了函数的参数。
很多 defer
语句都可以在编译期间判断是否被执行,如果函数中的 defer
语句都会在编译期间确定,中间代码生成阶段就会直接调用 cmd/compile/internal/gc.state.openDeferExit
在函数返回前生成判断 deferBits
的代码,也就是上述伪代码中的后半部分。
不过当程序遇到运行时才能判断的条件语句时,我们仍然需要由运行时的 runtime.deferreturn
决定是否执行 defer
关键字:
1
2
3
4
5
6
7
8
9
10
11
12
|
func deferreturn(arg0 uintptr) {
gp := getg()
d := gp._defer
sp := getcallersp()
if d.openDefer {
runOpenDeferFrame(gp, d)
gp._defer = d.link
freedefer(d)
return
}
...
}
|
该函数为开放编码做了特殊的优化,运行时会调用 runtime.runOpenDeferFrame
执行活跃的开放编码延迟函数,该函数会执行以下的工作:
- 从
runtime._defer
结构体中读取 deferBits
、函数 defer
数量等信息;
- 在循环中依次读取函数的地址和参数信息并通过
deferBits
判断该函数是否需要被执行;
- 调用
runtime.reflectcallSave
调用需要执行的 defer
函数;
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
|
func runOpenDeferFrame(gp *g, d *_defer) bool {
fd := d.fd
...
deferBitsOffset, fd := readvarintUnsafe(fd)
nDefers, fd := readvarintUnsafe(fd)
deferBits := *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset)))
for i := int(nDefers) - 1; i >= 0; i-- {
var argWidth, closureOffset, nArgs uint32 // 读取函数的地址和参数信息
argWidth, fd = readvarintUnsafe(fd)
closureOffset, fd = readvarintUnsafe(fd)
nArgs, fd = readvarintUnsafe(fd)
if deferBits&(1<<i) == 0 {
...
continue
}
closure := *(**funcval)(unsafe.Pointer(d.varp - uintptr(closureOffset)))
d.fn = closure
...
deferBits = deferBits &^ (1 << i)
*(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) = deferBits
p := d._panic
reflectcallSave(p, unsafe.Pointer(closure), deferArgs, argWidth)
if p != nil && p.aborted {
break
}
d.fn = nil
memclrNoHeapPointers(deferArgs, uintptr(argWidth))
...
}
return done
}
|
总结
三种机制:
- 堆上分配 · 1.1 ~ 1.12
- 栈上分配 · 1.13
- 开放编码 · 1.14 ~ 现在
painc&&recovery
panic
能够改变程序的控制流,调用 panic
后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer
;
recover
可以中止 panic
造成的程序崩溃。它是一个只能在 defer
中发挥作用的函数,在其他作用域中调用不会发挥作用;
现象
panic
只会触发当前 Goroutine 的 defer
;
recover
只有在 defer
中调用才会生效;
panic
允许在 defer
中嵌套多次调用;
如果代码为:
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
|
package main
import (
"time"
)
func main() {
println("start main")
defer println("in main")
testChan := make(chan int, 10)
go func() {
for {
select {
case val, ok := <-testChan:
println(val, ok)
}
}
}()
go func() {
defer println("in goroutine")
panic("")
}()
time.Sleep(1 * time.Second)
}
|
设置一个env:GOTRACEBACK=1
官方解释:
The GOTRACEBACK variable controls the amount of output generated when a Go program fails due to an unrecovered panic or an unexpected runtime condition. By default, a failure prints a stack trace for the current goroutine
, eliding functions internal to the run-time system, and then exits with exit code 2. The failure prints stack traces for all goroutines if there is no current goroutine or the failure is internal to the run-time. GOTRACEBACK=none omits the goroutine stack traces entirely
. GOTRACEBACK=single (the default) behaves as described above.
GOTRACEBACK=all adds stack traces for all user-created goroutines
. GOTRACEBACK=system is like “all” but adds stack frames for run-time functions and shows goroutines created internally by the run-time
. GOTRACEBACK=crash is like “system” but crashes in an operating system-specific manner instead of exiting.
For example, on Unix systems, the crash raises SIGABRT to trigger a core dump. For historical reasons, the GOTRACEBACK settings 0, 1, and 2 are synonyms for none, all, and system, respectively. The runtime/debug package’s SetTraceback function allows increasing the amount of output at run time, but it cannot reduce the amount below that specified by the environment variable.
也就是GOTRACEBACK可以有如下取值:
- none(0):屏蔽崩溃的栈输出
- single(默认值):只输出当前goroutine的栈
- all(1):输出所有用户创建的goroutines的栈
- system(2):除了all,还有一些系统级别的goroutines,例如垃圾回收goroutine
- crash:除了system的输出,还可以包括core文件,可以用于定位问题
则其输出为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
start main
in goroutine
panic:
goroutine 6 [running]:
main.main.func2()
/Users/hxsup/go/src/github.com/hTangle/test-go/main.go:20 +0x7b
created by main.main
/Users/hxsup/go/src/github.com/hTangle/test-go/main.go:18 +0xd3
goroutine 1 [sleep]:
time.Sleep(0x3b9aca00)
/usr/local/go/src/runtime/time.go:193 +0x12e
main.main()
/Users/hxsup/go/src/github.com/hTangle/test-go/main.go:23 +0xdd
goroutine 5 [chan receive]:
main.main.func1()
/Users/hxsup/go/src/github.com/hTangle/test-go/main.go:13 +0x27
created by main.main
/Users/hxsup/go/src/github.com/hTangle/test-go/main.go:10 +0xc5
|
可以看到,函数执行了go函数中的defer函数,但是没有执行main中的defer函数,说明panic
只会触发当前 Goroutine 的 defer
。
panic数据结构
panic
关键字在 Go 语言的源代码是由数据结构 runtime._panic
表示的。每当我们调用 panic
都会创建一个如下所示的数据结构存储相关信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// A _panic holds information about an active panic.
//
// A _panic value must only ever live on the stack.
//
// The argp and link fields are stack pointers, but don't need special
// handling during stack growth: because they are pointer-typed and
// _panic values only live on the stack, regular stack pointer
// adjustment takes care of them.
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // whether this panic is over
aborted bool // the panic was aborted
goexit bool
}
|
argp
是指向 defer
调用时参数的指针;
arg
是调用 panic
时传入的参数;
link
指向了更早调用的 runtime._panic
结构;
recovered
表示当前 runtime._panic
是否被 recover
恢复;
aborted
表示当前的 panic
是否被强行终止;
从数据结构中的 link
字段我们就可以推测出以下的结论:panic
函数可以被连续多次调用,它们之间通过 link
可以组成链表。
结构体中的 pc
、sp
和 goexit
三个字段都是为了修复 runtime.Goexit
带来的问题引入的。runtime.Goexit
能够只结束调用该函数的 Goroutine 而不影响其他的 Goroutine,但是该函数会被 defer
中的 panic
和 recover
取消,引入这三个字段就是为了保证该函数的一定会生效。
程序崩溃
编译器会将关键字 panic
转换成 runtime.gopanic
,该函数的执行过程包含以下几个步骤:
- 创建新的
runtime._panic
并添加到所在 Goroutine 的 _panic
链表的最前面;
- 在循环中不断从当前 Goroutine 的
_defer
中链表获取 runtime._defer
并调用 runtime.reflectcall
运行延迟调用函数;
- 调用
runtime.fatalpanic
中止整个程序;
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
|
func gopanic(e interface{}) {
gp := getg()
...
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
for {
d := gp._defer
if d == nil {
break
}
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
if p.recovered {
...
}
}
fatalpanic(gp._panic)
*(*int)(nil) = 0
}
|
runtime.fatalpanic
实现了无法被恢复的程序崩溃,它在中止程序之前会通过 runtime.printpanics
打印出全部的 panic
消息以及调用时传入的参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
if startpanic_m() && msgs != nil {
atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs)
}
if dopanic_m(gp, pc, sp) {
crash()
}
exit(2)
}
|
打印崩溃消息后会调用 runtime.exit
退出当前程序并返回错误码 2,程序的正常退出也是通过 runtime.exit
实现的。
崩溃恢复
编译器会将关键字 recover
转换成 runtime.gorecover
:
1
2
3
4
5
6
7
8
9
|
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
|
该函数的实现很简单,如果当前 Goroutine 没有调用 panic
,那么该函数会直接返回 nil
,这也是崩溃恢复在非 defer
中调用会失效的原因。在正常情况下,它会修改 runtime._panic
的 recovered
字段,runtime.gorecover
函数中并不包含恢复程序的逻辑,程序的恢复是由 runtime.gopanic
函数负责的:
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
|
func gopanic(e interface{}) {
...
for {
// 执行延迟调用函数,可能会设置 p.recovered = true
...
pc := d.pc
sp := unsafe.Pointer(d.sp)
...
if p.recovered {
gp._panic = p.link
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil {
gp.sig = 0
}
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed")
}
}
...
}
|
当我们在调用 defer
关键字时,调用时的栈指针 sp
和程序计数器 pc
就已经存储到了 runtime._defer
结构体中,这里的 runtime.gogo
函数会跳回 defer
关键字调用的位置。
runtime.recovery
在调度过程中会将函数的返回值设置成 1。从 runtime.deferproc
的注释中我们会发现,当 runtime.deferproc
函数的返回值是 1 时,编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturn
:
1
2
3
4
|
func deferproc(siz int32, fn *funcval) {
...
return0()
}
|
跳转到 runtime.deferreturn
函数之后,程序就已经从 panic
中恢复了并执行正常的逻辑,而 runtime.gorecover
函数也能从 runtime._panic
结构中取出了调用 panic
时传入的 arg
参数并返回给调用方。
总结
- 编译器会负责做转换关键字的工作;
- 将
panic
和 recover
分别转换成 runtime.gopanic
和 runtime.gorecover
;
- 将
defer
转换成 runtime.deferproc
函数;
- 在调用
defer
的函数末尾调用 runtime.deferreturn
函数;
- 在运行过程中遇到
runtime.gopanic
方法时,会从 Goroutine 的链表依次取出 runtime._defer
结构体并执行;
- 如果调用延迟执行函数时遇到了
runtime.gorecover
就会将_panic.recovered
标记成 true 并返回panic
的参数;
- 在这次调用结束之后,
runtime.gopanic
会从 runtime._defer
结构体中取出程序计数器 pc
和栈指针 sp
并调用 runtime.recovery
函数进行恢复程序;
runtime.recovery
会根据传入的 pc
和 sp
跳转回 runtime.deferproc
;
- 编译器自动生成的代码会发现
runtime.deferproc
的返回值不为 0,这时会跳回 runtime.deferreturn
并恢复到正常的执行流程;
- 如果没有遇到
runtime.gorecover
就会依次遍历所有的 runtime._defer
,并在最后调用 runtime.fatalpanic
中止程序、打印 panic
的参数并返回错误码 2;
参考链接: