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)
	}
}

其输出为:

1
2
3
4
5
4
3
2
1
0

所以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()
}

其输出为:

1
151ns

而我们预期的输出是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
}

上面这个函数的输出为:

1
2
func 10
direct 5

虽然调用 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 是参数和结果的内存大小;
  • sppc 分别代表栈指针和调用方的程序计数器;
  • fndefer 关键字中传入的函数;
  • _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.callResultcmd/compile/internal/gc.state.call,这表示 defer 在编译器看来也是函数调用。

cmd/compile/internal/gc.state.call 会负责为所有函数和方法调用生成中间代码,它的工作包括以下内容:

  1. 获取需要执行的函数名、闭包指针、代码指针和函数调用的接收方;
  2. 获取栈地址并将函数或者方法的参数写入栈中;
  3. 使用 cmd/compile/internal/gc.state.newValue1A 以及相关函数生成函数调用的中间代码;
  4. 如果当前调用的函数是 defer,那么会单独生成相关的结束代码块;
  5. 获取函数的返回值地址并结束当前调用;
 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.deferprocruntime.newdefer 的作用是想尽办法获得 runtime._defer 结构体,这里包含三种路径:

  1. 从调度器的延迟调用缓存池 sched.deferpool 中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中;
  2. 从 Goroutine 的延迟调用缓存池 pp.deferpool 中取出结构体;
  3. 通过 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 关键字的方法,它不是在所有的场景下都会开启的,开放编码只会在满足以下的条件时启用:

  1. 函数的 defer 数量少于或者等于 8 个;
  2. 函数的 defer 关键字不能在循环中执行;
  3. 函数的 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 执行活跃的开放编码延迟函数,该函数会执行以下的工作:

  1. runtime._defer 结构体中读取 deferBits、函数 defer 数量等信息;
  2. 在循环中依次读取函数的地址和参数信息并通过 deferBits 判断该函数是否需要被执行;
  3. 调用 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
}

总结

三种机制:

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
}
  1. argp 是指向 defer 调用时参数的指针;
  2. arg 是调用 panic 时传入的参数;
  3. link 指向了更早调用的 runtime._panic 结构;
  4. recovered 表示当前 runtime._panic 是否被 recover 恢复;
  5. aborted 表示当前的 panic 是否被强行终止;

从数据结构中的 link 字段我们就可以推测出以下的结论:panic 函数可以被连续多次调用,它们之间通过 link 可以组成链表。

结构体中的 pcspgoexit 三个字段都是为了修复 runtime.Goexit 带来的问题引入的。runtime.Goexit 能够只结束调用该函数的 Goroutine 而不影响其他的 Goroutine,但是该函数会被 defer 中的 panicrecover 取消,引入这三个字段就是为了保证该函数的一定会生效。

程序崩溃

编译器会将关键字 panic 转换成 runtime.gopanic,该函数的执行过程包含以下几个步骤:

  1. 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面;
  2. 在循环中不断从当前 Goroutine 的 _defer 中链表获取 runtime._defer 并调用 runtime.reflectcall 运行延迟调用函数;
  3. 调用 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._panicrecovered 字段,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 参数并返回给调用方。

总结

  1. 编译器会负责做转换关键字的工作;
    1. panicrecover 分别转换成 runtime.gopanicruntime.gorecover
    2. defer 转换成 runtime.deferproc 函数;
    3. 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
  2. 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
  3. 如果调用延迟执行函数时遇到了 runtime.gorecover就会将_panic.recovered标记成 true 并返回panic的参数;
    1. 在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;
    2. runtime.recovery 会根据传入的 pcsp 跳转回 runtime.deferproc
    3. 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
  4. 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;

参考链接: