defer 的使用与原理

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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值