defer 的使用与原理

167 阅读11分钟

1. 前言

defer 是 go 语言的关键字,被 defer 修饰的 func,将会在函数返回之前执行。 defer 具有以下特点

  • 延迟执行
  • 参数预计算
  • 同一 goroutine 中多个 defer 的执行顺序依照 FILO 规则

2. 使用场景

2.1. 资源释放

下边的程序是打开文件,并在执行一系列操作后,将文件关闭。

func readFile() {
  src,err := os.Open("filename")
  if err != nil {
    return
  }
  // do something
  src.Close()
}

代码通过 os.Open 打开一个文件,在执行一系列操作后,在最后通过 src.Close 方法将资源释放。正常情况下,该程序都能够正常运行到 src.Close。但在执行一系列操作过程中,出现异常并退出程序后,文件资源将无法被释放。一般,我们期望在程序退出前能够执行资源释放操作,而使用 defer 能达到这种目的。将上边程序修改如下,在成功打开文件之后,无论后边程序是否能够正常运行 ,在退出前,都能够将文件资源释放。

func readFile() {
  src,err := os.Open("filename")
  if err != nil {
    return
  }
  defer src.Close()
  // do something
}

同样的问题还会发生在使用 lock, channel 的情况下,一旦加锁后发生错误,将可能导致严重的死锁问题,多次创建未被 close 掉的 channel,长期下来,将导致严重的内存泄漏问题。

2.2. 异常捕获

程序可能存在 panic ,如除 0 运算、数组越界访问 、对空指针取值等,这些异常将导致程序退出。某些时候,我们期望捕获这些异常,并让程序正常执行。下边为模拟程序运行过程中发生 panic。

func doPanic() {
  panic("panic")
  fmt.Println("do panic success")
}

将会得到输出如下,terminal 将会打印出程序异常退出栈追踪信息 。

panic: panic

goroutine 1 [running]:
main.doPanic(...)
        /Users/xxx/Documents/workspace/go/notecode/main.go:29
main.main()
        /Users/xxx/Documents/workspace/go/notecode/main.go:8 +0x27
exit status 2

若期望程序能够正常运行,可将程序修改如下:

func doPanic() {
  defer func(){
    if err := recover();err != nil {
      fmt.Println("recover")
    }
  }()
  panic("panic")
  fmt.Println("do panic success")
}

输出如下,表明程序异常后,仍能被正常调用执行。

recover

3. 原理解析

本文使用的 go version 为 go1.17.10

3.1. defer 结构定义

每个协程对象中,存在一个 _defer 指针,该指针指向与该协程相关联的defer,每个 defer 的调用只与当前协程有关 ,与其它协程无关。

type g struct  {
  _defer  *_defer
}

_defer 结构定义在 runtime/runtime2.go 文件中,具体定义如下:

type _defer struct {
  siz int32 // 参数大小
  started bool  // 当前 defer 是否开始被执行
  heap bool // 是否被分配到堆中
  openDefer bool // 是否开放式编程
  sp uintptr  // sp 寄存器 
  pc uintptr // pc 寄存器
  fn *funcval // defer 运行的 func
  _panic *_panic 
  link *_defer // 下一个 _defer
  // 与开放式编程相关的参数
  fd unsafe.Pointer  
  varp uintptr
  framepc uintptr
}

多个 defer 与 goroutine 的关联如下:

g_defer.png

3.2. 延迟执行

defer 后的函数不会立即执行,而是待函数执行结束后再调用。

func doDefer() {
	defer func() {
		fmt.Println("do defer")
	}()
	fmt.Println("doDefer normal")
}

输出结果如下,说明 defer 后的函数待函数执行返回时才执行,因此可以很好地用作资源释放。

doDefer normal
do defer

通过 go tool compile -N -l main.go + go tool objdump main.o运行函数,观察main.go 的汇编情况。

defer_delay.png

在执行 doDefer 函数的过程中,先是调用 runtime.deferprocStack() 方法,待 fmt.Println()打印结束后,再执行 runtime.deferreturn。编译器会自动在函数的最后插入 deferreturn, 这也是 defer 为何具有延迟执行特性的原因。

打开 runtime/panic.go 文件, 查看 deferprocStack 函数的内容,如下:

func deferprocStack(d *_defer) {
	gp := getg() // 获取 g,判断是否在当前栈上
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("defer on system stack")
	}
	if goexperiment.RegabiDefer && d.siz != 0 {
		throw("defer with non-empty frame")
	}
	// siz and fn are already set.
	// The other fields are junk on entry to deferprocStack and
	// are initialized here.
  // defer 未开始
	d.started = false
  // defer 不在堆上
	d.heap = false
  // defer 没有开放式编程
	d.openDefer = false
  // 设置 sp
	d.sp = getcallersp()
  // 设置 pc
	d.pc = getcallerpc()
  // 没有开放式编程,framepc,varp 等属性都是为 0
	d.framepc = 0
	d.varp = 0
	// The lines below implement:
	//   d.panic = nil
	//   d.fd = nil
	//   d.link = gp._defer
	//   gp._defer = d
	// But without write barriers. The first three are writes to
	// the stack so they don't need a write barrier, and furthermore
	// are to uninitialized memory, so they must not use a write barrier.
	// The fourth write does not require a write barrier because we
	// explicitly mark all the defer structures, so we don't need to
	// keep track of pointers to them with a write barrier.
	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
	*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
  // 将当前 defer 插入到链表头部
	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

  // 在 return0 之后,将不能再设置任何代码
	return0()
	// No code can go here - the C return register has
	// been set and must not be clobbered.
}

deferreturn 内容如下:

func deferreturn() {
  // 获取当前 g
	gp := getg()
  // 获取 g 的 _defer 头节点
	d := gp._defer
  // 不存在 _defer 链表,直接结束
	if d == nil {
		return
	}
	sp := getcallersp()
	if d.sp != sp {
		return
	}
	if d.openDefer {
		done := runOpenDeferFrame(gp, d)
		if !done {
			throw("unfinished open-coded defers in deferreturn")
		}
		gp._defer = d.link
		freedefer(d)
		return
	}

	// 移动参数,在这个点后的所有调用的内容不得包含堆栈溢出
  // 因为垃圾收集器不会知道参数的形式直到 jumpdefer 执行完
	argp := getcallersp() + sys.MinFrameSize
	switch d.siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		*(*uintptr)(unsafe.Pointer(argp)) = *(*uintptr)(deferArgs(d))
	default:
    // 在栈上的 _defer,与它关联的参数立即存储在内存的  _defer head 后 
		memmove(unsafe.Pointer(argp), deferArgs(d), uintptr(d.siz))
	}
	fn := d.fn
	d.fn = nil
	gp._defer = d.link
  // 清空当前 _defer
	freedefer(d)
	// If the defer function pointer is nil, force the seg fault to happen
	// here rather than in jmpdefer. gentraceback() throws an error if it is
	// called with a callback on an LR architecture and jmpdefer is on the
	// stack, because the stack trace can be incorrect in that case - see
	// issue #8153).
	_ = fn.fn
  // 执行 _defer 关联的 func 和下一个 _defer
	jmpdefer(fn, argp)
}

3.3. 参数预计算

下边程序的 defer 函数中传入参数 a ,参数 a 在后边执行加一操作。

package main

import "fmt"

func main(){
   a := 1
   defer DeferA(a)
   a += 1
   fmt.Printf("normal run,a:%d\n",a)
}

输出如下,在 deferA 中输出的参数 a,并没有随着 a 的改变,而输出 2,在

normal run,a:2
DeferA,a :1

3.4. 多次 defer 与 LIFO 执行顺序一致

package main

import "fmt"

func main(){
   defer DeferA()
   defer DeferB()
   defer DeferC()
   fmt.Println("normal run")
}
func DeferA(){
   fmt.Println("DeferA")
}
func  DeferB()  {
   fmt.Println("DeferB")
}

func DeferC() {
   fmt.Println("DeferC")
}

输出如下,越靠前的 defer ,其输出越靠后

normal run
DeferC
DeferB
DeferA

查看其汇编代码;

  main.go:5             0x115f                  4c8da42458ffffff                LEAQ 0xffffff58(SP), R12        [3:3]R_USEIFACE:type.string
  main.go:5             0x1167                  4d3b6610                        CMPQ 0x10(R14), R12
  main.go:5             0x116b                  0f863b010000                    JBE 0x12ac
  main.go:5             0x1171                  4881ec28010000                  SUBQ $0x128, SP
  main.go:5             0x1178                  4889ac2420010000                MOVQ BP, 0x120(SP)
  main.go:5             0x1180                  488dac2420010000                LEAQ 0x120(SP), BP
  main.go:6             0x1188                  488d0d00000000                  LEAQ 0(IP), CX                  [3:7]R_PCREL:"".DeferA·f
  main.go:6             0x118f                  48898c24c0000000                MOVQ CX, 0xc0(SP)
  main.go:6             0x1197                  488d8424a8000000                LEAQ 0xa8(SP), AX
  main.go:6             0x119f                  e800000000                      CALL 0x11a4                     [1:5]R_CALL:runtime.deferprocStack<1>
  main.go:6             0x11a4                  85c0                            TESTL AX, AX
  main.go:6             0x11a6                  0f85eb000000                    JNE 0x1297
  main.go:6             0x11ac                  eb00                            JMP 0x11ae
  main.go:7             0x11ae                  488d0d00000000                  LEAQ 0(IP), CX                  [3:7]R_PCREL:"".DeferB·f
  main.go:7             0x11b5                  48894c2478                      MOVQ CX, 0x78(SP)
  main.go:7             0x11ba                  488d442460                      LEAQ 0x60(SP), AX
  main.go:7             0x11bf                  e800000000                      CALL 0x11c4                     [1:5]R_CALL:runtime.deferprocStack<1>
  main.go:7             0x11c4                  85c0                            TESTL AX, AX
  main.go:7             0x11c6                  0f85b6000000                    JNE 0x1282
  main.go:7             0x11cc                  eb00                            JMP 0x11ce
  main.go:8             0x11ce                  488d0d00000000                  LEAQ 0(IP), CX                  [3:7]R_PCREL:"".DeferC·f
  main.go:8             0x11d5                  48894c2430                      MOVQ CX, 0x30(SP)
  main.go:8             0x11da                  488d442418                      LEAQ 0x18(SP), AX
  main.go:8             0x11df                  e800000000                      CALL 0x11e4                     [1:5]R_CALL:runtime.deferprocStack<1>
  main.go:8             0x11e4                  85c0                            TESTL AX, AX
  main.go:8             0x11e6                  0f8581000000                    JNE 0x126d
  main.go:8             0x11ec                  eb00                            JMP 0x11ee
  main.go:9             0x11ee                  440f11bc24f8000000              MOVUPS X15, 0xf8(SP)
  main.go:9             0x11f7                  488d8424f8000000                LEAQ 0xf8(SP), AX
  main.go:9             0x11ff                  48898424f0000000                MOVQ AX, 0xf0(SP)
  main.go:9             0x1207                  8400                            TESTB AL, 0(AX)
  main.go:9             0x1209                  488d1500000000                  LEAQ 0(IP), DX                  [3:7]R_PCREL:type.string
  main.go:9             0x1210                  48899424f8000000                MOVQ DX, 0xf8(SP)
  main.go:9             0x1218                  488d1500000000                  LEAQ 0(IP), DX                  [3:7]R_PCREL:""..stmp_0<1>
  main.go:9             0x121f                  4889942400010000                MOVQ DX, 0x100(SP)
  main.go:9             0x1227                  8400                            TESTB AL, 0(AX)
  main.go:9             0x1229                  eb00                            JMP 0x122b
  main.go:9             0x122b                  4889842408010000                MOVQ AX, 0x108(SP)
  main.go:9             0x1233                  48c784241001000001000000        MOVQ $0x1, 0x110(SP)
  main.go:9             0x123f                  48c784241801000001000000        MOVQ $0x1, 0x118(SP)
  main.go:9             0x124b                  bb01000000                      MOVL $0x1, BX
  main.go:9             0x1250                  4889d9                          MOVQ BX, CX
  main.go:9             0x1253                  e800000000                      CALL 0x1258                     [1:5]R_CALL:fmt.Println
  main.go:10            0x1258                  e800000000                      CALL 0x125d                     [1:5]R_CALL:runtime.deferreturn<1>
  main.go:10            0x125d                  488bac2420010000                MOVQ 0x120(SP), BP
  main.go:10            0x1265                  4881c428010000                  ADDQ $0x128, SP
  main.go:10            0x126c                  c3                              RET
  main.go:8             0x126d                  e800000000                      CALL 0x1272                     [1:5]R_CALL:runtime.deferreturn<1>
  main.go:8             0x1272                  488bac2420010000                MOVQ 0x120(SP), BP
  main.go:8             0x127a                  4881c428010000                  ADDQ $0x128, SP
  main.go:8             0x1281                  c3                              RET

针对 DeferA, DeferB,DeferC,编译器分布将关键字 defer 转换成 runtime.deferprocStack,并在运行末尾进行三次 deferreturn。从3.2中可知,runtime.deferprocStack 每次都创建一个 _defer ,并将其加入到 goroutine 的 _defer 链表头部,执行完三次 runtime.deferprocStack 后,_defer 链表的情况是 DeferC -> DeferB -> DeferA 。同样从 3.2 中可知 runtime.deferreturn 每次取出 goroutine 的 _defer 并调用执行其关联的 function。因此,程序中三个 defer 的执行顺序是 DeferC、DeferB、DeferA。

4. 捕捉异常

4.1. 如何捕捉异常

一般情况下程序发生异常,将会终止整个请求运行。如下所示:

package main

import "fmt"

func main() {
	fmt.Println("start to do panic")
	doPanic()
}

func doPanic() {
	fmt.Println("hello")
	panic("panic")
}

输出结果如下:

start to do panic
hello
panic: panic

goroutine 1 [running]:
main.doPanic(...)
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:12
main.main()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:7 +0xee
exit status 2

程序将退出,并将整个执行调用栈打印出来。若期望程序能够正常运行,可以 recover 进行捕获,使用方式如下:

package main

import "fmt"

func main() {
	fmt.Println("start to do panic")
	doPanic()
}

func doPanic() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("recover")
		}
	}()
	fmt.Println("hello")
	panic("panic")
}

输出结果如下,可发现,程序执行过程中发生异常后被捕获,程序能正常执行。

start to do panic
hello
recover

recover 函数必须结合 defer 使用,未定义在 defer func 里的 recover,将无法捕获异常,示例如下:

package main

import "fmt"

func main() {
	fmt.Println("start to do panic")
	doPanic()
}

func doPanic() {
	if err := recover(); err != nil {
		fmt.Println("recover")
	}
	fmt.Println("hello")
	panic("panic")
}

此时程序运行过程中发生 panic, 但并未被 recover 住,程序仍被终止并输出异常调用栈。

start to do panic
hello
panic: panic

goroutine 1 [running]:
main.doPanic()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:15 +0x103
main.main()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:7 +0x7a
exit status 2

4.2. defer 与 panic 的执行顺序

下边是多次 defer 尾随 panic 发生,观察发生 panic 时程序的执行顺序:

package main

import "fmt"

func main() {
	doPanic()
}

func doPanic() {
	defer deferA()
	defer deferB()
	panic("panic")
}

func deferA() {
	fmt.Println("deferA")
}

func deferB() {
	fmt.Println("deferB")
}

程序将按 LIFO 的顺序先执行 defer,待 defer 执行完后,打印 panic 关联的堆栈信息。

deferB
deferA
panic: panic

goroutine 1 [running]:
main.doPanic()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:12 +0x97
main.main()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:6 +0x20
exit status 2

_panic 的结构定义如下:

type _panic struct {
	argp      unsafe.Pointer // 指向 defer 在 panic 期间运行的参数空间
	arg       interface{}    // panic 中的参数
	link      *_panic        // 链接更早的 _panic
	recovered bool           // panic 是否被捕获
	aborted   bool           // panic 被终止
}

通过汇编编译得到对应的.o文件如下:

TEXT %22%22.doPanic(SB) gofile../Users/gertieliang/GolandProjects/LearnGoProject/main.go
  main.go:9             0xa98                   65488b0c2500000000      MOVQ GS:0, CX           [5:9]R_TLS_LE
  main.go:9             0xaa1                   488d4424f8              LEAQ -0x8(SP), AX
  main.go:9             0xaa6                   483b4110                CMPQ 0x10(CX), AX
  main.go:9             0xaaa                   0f86af000000            JBE 0xb5f
  main.go:9             0xab0                   4881ec88000000          SUBQ $0x88, SP
  main.go:9             0xab7                   4889ac2480000000        MOVQ BP, 0x80(SP)
  main.go:9             0xabf                   488dac2480000000        LEAQ 0x80(SP), BP
  main.go:10            0xac7                   c744244800000000        MOVL $0x0, 0x48(SP)
  main.go:10            0xacf                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:%22%22.deferA·f
  main.go:10            0xad6                   4889442460              MOVQ AX, 0x60(SP)
  main.go:10            0xadb                   488d442448              LEAQ 0x48(SP), AX
  main.go:10            0xae0                   48890424                MOVQ AX, 0(SP)
  main.go:10            0xae4                   e800000000              CALL 0xae9              [1:5]R_CALL:runtime.deferprocStack
  main.go:10            0xae9                   85c0                    TESTL AX, AX
  main.go:10            0xaeb                   755c                    JNE 0xb49
  main.go:10            0xaed                   eb00                    JMP 0xaef
  main.go:11            0xaef                   c744241000000000        MOVL $0x0, 0x10(SP)
  main.go:11            0xaf7                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:%22%22.deferB·f
  main.go:11            0xafe                   4889442428              MOVQ AX, 0x28(SP)
  main.go:11            0xb03                   488d442410              LEAQ 0x10(SP), AX
  main.go:11            0xb08                   48890424                MOVQ AX, 0(SP)
  main.go:11            0xb0c                   e800000000              CALL 0xb11              [1:5]R_CALL:runtime.deferprocStack
  main.go:11            0xb11                   85c0                    TESTL AX, AX
  main.go:11            0xb13                   751e                    JNE 0xb33
  main.go:11            0xb15                   eb00                    JMP 0xb17
  main.go:12            0xb17                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:type.string
  main.go:12            0xb1e                   48890424                MOVQ AX, 0(SP)
  main.go:12            0xb22                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:%22%22..stmp_0
  main.go:12            0xb29                   4889442408              MOVQ AX, 0x8(SP)
  main.go:12            0xb2e                   e800000000              CALL 0xb33              [1:5]R_CALL:runtime.gopanic
  main.go:11            0xb33                   90                      NOPL
  main.go:11            0xb34                   e800000000              CALL 0xb39              [1:5]R_CALL:runtime.deferreturn
  main.go:11            0xb39                   488bac2480000000        MOVQ 0x80(SP), BP
  main.go:11            0xb41                   4881c488000000          ADDQ $0x88, SP
  main.go:11            0xb48                   c3                      RET
  main.go:10            0xb49                   90                      NOPL
  main.go:10            0xb4a                   e800000000              CALL 0xb4f              [1:5]R_CALL:runtime.deferreturn
  main.go:10            0xb4f                   488bac2480000000        MOVQ 0x80(SP), BP
  main.go:10            0xb57                   4881c488000000          ADDQ $0x88, SP
  main.go:10            0xb5e                   c3                      RET
  main.go:9             0xb5f                   e800000000              CALL 0xb64              [1:5]R_CALL:runtime.morestack_noctxt
  main.go:9             0xb64                   e92fffffff              JMP %22%22.doPanic(SB)

可以发现,编译时会将 panic 关键字转换成 runtime.gopanic 函数。程序发生 panic 后,依次调用 runtime.deferreturn。

查看 runtime.gopanic 的内容如下:

func gopanic(e interface{}) {
	gp := getg()
	// 省略一些列操作 ...
        // 创建一个 _panic 对象,并加到 goroutine 的 _panic 中
        var p _panic
	p.arg = e
	p.link = gp._panic
	gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

	atomic.Xadd(&runningPanicDefers, 1)
	for {
		d := gp._defer
                // goroutine 的 defer 队列为空,退出
		if d == nil {
			break
		}
                // 如果 defer 运行中
                if d.started {
			if d._panic != nil {
				d._panic.aborted = true
			}
			d._panic = nil
			d.fn = nil
			gp._defer = d.link
			freedefer(d)
			continue
		}
                // 将 goroutine 队头的 _defer 的 started 状态置为 true,并执行调用 _defer 
		d.started = true
		d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

		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")
		}
                // 置空当前 _defer,从 goroutine 的 _defer 中移除,并 free 调
		d._panic = nil
		d.fn = nil
		gp._defer = d.link
		pc := d.pc
		sp := unsafe.Pointer(d.sp) 
		freedefer(d)
                // 如果 _panic 被捕获,则从 goroutine 的 _panic 链表中移除当前 _panic
		if p.recovered {
			atomic.Xadd(&runningPanicDefers, -1)
			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
                        // 调用 recover 内容
			mcall(recovery)
			throw("recovery failed") // mcall should not return
		}
	}
        // 省略一些列操作
}

runtime.gopanic 内容可以简单概括为执行一下内容:

  • 创建 _panic 对象,并加到 goroutine 关联的 _panic 的链表头
  • 循环取出 goroutine 关联的 _defer 链表头的 _defer 执行调用,并 free 调用完成的 _defer

4.3. defer 过程中发生 panic

在执行 _defer 过程中, 程序仍可能发生 panic,下边为在执行 defer 过程中发生 panic 的示例。

package main

import "fmt"

func main() {
	doPanic()
}

func doPanic() {
	defer deferA()
	defer deferB()
	panic("panicM")
}

func deferA() {
	fmt.Println("deferA")
}

func deferB() {
	panic("panicB")
}

在 defer 过程中发生 panic,并不会影响程序继续执行下一个 defer 内容,待最后一个 defer 被执行完后,将按 panic 发生的先后顺序,依次打印 panic 情况。

deferA
panic: panic
        panic: deferB

goroutine 1 [running]:
main.deferB()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:20 +0x39
panic(0x10ab020, 0x10e95d0)
        /usr/local/go/src/runtime/panic.go:679 +0x1b2
main.doPanic()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:12 +0x97
main.main()
        /Users/gertieliang/GolandProjects/LearnGoProject/main.go:6 +0x20
exit status 2

此时程序的运行过程如下:

  • 将 panicM 对象加入 goroutine 的 _panic 链表头
  • 将 deferB 的 started 状态置为 true, 将 deferB 的 _panic 属性指向当前 panic, 执行调用 deferB
  • 调用 deferB 过程中发生 panicB, 将 paincB 插入 goroutine 的 _panic 链表头,并将 panicB.link 执行 panicM
  • 取出 goroutine 的 _defer 头,发现此时 panicB 的状态为 started,将 deferB._panic 即 panicB 的 aborted 状态置为 true, 置空 deferB 并 free
  • 继续取出 goroutine 的 _defer 头 deferA,修改 deferA 的状态为 stated, deferA._panic = panicB, 执行调用 deferA,待 deferA 调用完成后,free deferA
  • 继续取 goroutine 的 _defer 头,发现链表为空,结束 _defer 链表的调用
  • 从头到尾,依次打印 goroutine 的 _panic 链表中未被 recoverd 的 panic 的堆栈信息。

4.4. 使用 recover 捕获 panic

runtime.gorecover 的执行如下所示:

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
}

gorecover 并不处理 _panic, 只将对应的 _panic 的 recovered 状态置为 true,具体处理仍交由 gopanic。

recover 未定义在 defer 中,将不能捕获异常,下边未将 recover 置于 defer 中

package main

import "fmt"

func main() {
	doPanic()
}

func doPanic() {
	if err := recover(); err != nil {
		fmt.Println("recover")
	}
	panic("panic")
}

生成汇编代码如下:

TEXT %22%22.doPanic(SB) gofile../Users/gertieliang/GolandProjects/LearnGoProject/main.go
  main.go:9             0x783                   65488b0c2500000000      MOVQ GS:0, CX           [5:9]R_TLS_LE
  main.go:9             0x78c                   483b6110                CMPQ 0x10(CX), SP
  main.go:9             0x790                   0f86bd000000            JBE 0x853
  main.go:9             0x796                   4883ec78                SUBQ $0x78, SP
  main.go:9             0x79a                   48896c2470              MOVQ BP, 0x70(SP)
  main.go:9             0x79f                   488d6c2470              LEAQ 0x70(SP), BP
  main.go:10            0x7a4                   488d842480000000        LEAQ 0x80(SP), AX
  main.go:10            0x7ac                   48890424                MOVQ AX, 0(SP)
  main.go:10            0x7b0                   e800000000              CALL 0x7b5              [1:5]R_CALL:runtime.gorecover
  main.go:10            0x7b5                   488b442408              MOVQ 0x8(SP), AX
  main.go:10            0x7ba                   488b4c2410              MOVQ 0x10(SP), CX
  main.go:10            0x7bf                   4889442438              MOVQ AX, 0x38(SP)
  main.go:10            0x7c4                   48894c2440              MOVQ CX, 0x40(SP)
  main.go:10            0x7c9                   4885c0                  TESTQ AX, AX
  main.go:10            0x7cc                   7502                    JNE 0x7d0
  main.go:10            0x7ce                   eb64                    JMP 0x834
  main.go:11            0x7d0                   0f57c0                  XORPS X0, X0
  main.go:11            0x7d3                   0f11442448              MOVUPS X0, 0x48(SP)
  main.go:11            0x7d8                   488d442448              LEAQ 0x48(SP), AX
  main.go:11            0x7dd                   4889442430              MOVQ AX, 0x30(SP)
  main.go:11            0x7e2                   8400                    TESTB AL, 0(AX)
  main.go:11            0x7e4                   488d0d00000000          LEAQ 0(IP), CX          [3:7]R_PCREL:type.string
  main.go:11            0x7eb                   48894c2448              MOVQ CX, 0x48(SP)
  main.go:11            0x7f0                   488d0d00000000          LEAQ 0(IP), CX          [3:7]R_PCREL:%22%22..stmp_0
  main.go:11            0x7f7                   48894c2450              MOVQ CX, 0x50(SP)
  main.go:11            0x7fc                   8400                    TESTB AL, 0(AX)
  main.go:11            0x7fe                   eb00                    JMP 0x800
  main.go:11            0x800                   4889442458              MOVQ AX, 0x58(SP)
  main.go:11            0x805                   48c744246001000000      MOVQ $0x1, 0x60(SP)
  main.go:11            0x80e                   48c744246801000000      MOVQ $0x1, 0x68(SP)
  main.go:11            0x817                   48890424                MOVQ AX, 0(SP)
  main.go:11            0x81b                   48c744240801000000      MOVQ $0x1, 0x8(SP)
  main.go:11            0x824                   48c744241001000000      MOVQ $0x1, 0x10(SP)
  main.go:11            0x82d                   e800000000              CALL 0x832              [1:5]R_CALL:fmt.Println
  main.go:11            0x832                   eb02                    JMP 0x836
  main.go:10            0x834                   eb00                    JMP 0x836
  main.go:13            0x836                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:type.string
  main.go:13            0x83d                   48890424                MOVQ AX, 0(SP)
  main.go:13            0x841                   488d0500000000          LEAQ 0(IP), AX          [3:7]R_PCREL:%22%22..stmp_1
  main.go:13            0x848                   4889442408              MOVQ AX, 0x8(SP)
  main.go:13            0x84d                   e800000000              CALL 0x852              [1:5]R_CALL:runtime.gopanic
  main.go:13            0x852                   90                      NOPL
  main.go:9             0x853                   e800000000              CALL 0x858              [1:5]R_CALL:runtime.morestack_noctxt
  main.go:9             0x858                   e926ffffff              JMP %22%22.doPanic(SB)

程序中 runtime.gorecover 的调用在程序发生 panic 之前,当程序发生 panic 后,并无 recover 对进行异常捕获。而是置于 defer 的 recover,将利用 defer 的延迟执行特点,在程序返回前执行调用 recover ,从而成功捕获 panic。