Go —— defer

defer

defer 语句用于延迟函数的调用,常用于关闭文件描述符、释放锁等资源释放场景。但 defer 关键字只能作用于函数或函数调用。

defer func(){						// 函数
    fmt.Print("Hello,World!")
}()

defer fmt.Print("Hello,World!")		// 函数调用

1. 执行机制

1.1 执行时机

在Go语言的函数中 return 语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer 语句执行的实际就在返回值操作后,RET指令前。具体如下图所示:

defer执行时机

1.2 行为规则

1)规则一:延迟函数的参数在 defer 语句出现时就已经确定了

示例如下:

func a() {
    i := 0
    defer fmt.Println(i)	// 程序运行打印 0
    i++
    return
}

defer 语句中的 fmt.Println() 参数 i 值 在 defer 出现时就已经确定了,实际上是复制了一份。后面对变量 i 的修改不会影响 fmt.Println() 函数的调用,依旧打印 0。

对于指针类型参数,此规则依然适用,只不过延迟函数的参数是一个地址值,在这种情况下,defer 后面的语句对变量的修改可能会影响延迟函数。

2)规则二:延迟函数按照后进先出 的顺序执行

设计 defer 的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如申请资源的顺序时 A→B→C,释放的顺序往往又要反向进行。这就是把 defer 设计成 LIFO 的原因

3)规则三:延迟函数可能操作主函数的具名返回值

定义 defer 的函数(下称主函数)可能有返回值,返回值可能有名字(具名返回值),也可能没有返回值(匿名返回值),延迟函数可能会影响返回值。

举个栗子:

func deferFuncReturn() (result int){
    i := 1
    
    defer func() {
        result++
    }()
    return i	// 程序返回 2
}

上面已经介绍过了 defer 的执行时机,该函数的 return 语句可以拆分成下面三行:

	result = i
	result++
	return

主函数有不同的返回方式,包括匿名返回值和具名返回值,但万变不离其宗,只要把 return 语句拆开都可以很好理解,下面分别举例说明:

(1)主函数拥有匿名返回值,返回字面值

一个主函数拥有一个匿名返回值,返回时使用字面值,这种情况下 defer 语句时无法操作返回值的

func foo() int {
	var i int
	
	defer func() {
		i++
	)()

	return 1
}

上面的 return 语句直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值

(2)主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名返回值,返回本地或全局变量,这种情况下 defer 语句可以引用返回值,反不会改变返回值

func foo() int {
	var i int
	
	defer func(){
		i++
	}
	return i
}

假定返回值变量为 anony ,上面的返回语句可以拆分为以下过程:

	annnoy = i
	i++
	return

函数返回 0

(3)主函数拥有具名返回值

主函数声明语句中带名字的返回值会被初始化为一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,则可能改变返回结果。

一个影响函数返回值的例子:

func foo() (ret int) {
	defer func() {
		ret++
	}()
	
	return 0     

上面的函数拆解出来如下所示:

	ret = 0
	ret++
	return

函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1

2. 实现原理

2.1 数据结构

源码包中 src/src/runtime/runtime2.go:_defer 定义了 defer 的数据结构

type _defer struct {
	...
	sp        uintptr // 函数栈指针
	pc        uintptr // 程序计数器
	fn        func()  // 函数地址
	link      *_defer // 指向自身结构的指针,用于链接多个 defer
	...
}

编译器会把每个延迟函数编译成一个 _defer 实例暂存到 goroutine 数据结构中,待函数结束时再逐个取出执行。
每个defer 语句对应一个 _defer 实例,多个实例使用指针 link 链接起来形成一个单链表,保存到 goroutine 数据结构中。
goroutine 的数据结构如下所示:

type g struct {
	...
	_defer *_defer // defer 链表
	...
}

每次插入 _defer 实例时均插入链表头部,函数执行结束时再依次从头部取出,从而实现后进先出的效果。
一个 goroutine 可能连续调用多个函数,defer 的添加过程跟上述流程一致,进入函数时添加 defer ,离开函数时取出 defer ,所以即便调用多个函数,也总是能保证 defer 是按 LIFO 方式执行的。

3. 小结

  • defer 定义的延迟函数参数在 defer 语句出现时就已经确定了
  • defer 定义的顺序与实际地执行顺序相反
  • return 不是原子操作,执行过程是:保存返回值 → 执行 defer → 执行 ret 跳转
  • 申请资源后立即使用 defer 关闭资源是一个好习惯
  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值