Go 学习之 defer 篇

1 | 后进先出

  • 这个大家都差不多明白了,后进入的 defer 函数先执行
func main() {
  defer func1()
  defer func2()
  defer func3()
  return 
}
- 执行顺序:func3 --> func2 --> func1

2 | 理解 defer 执行的时机

  • return 函数并不是个「原子操作」,拥有 defer 函数的 return 其运行过程如下
    • 先设置返回值
    • 执行defer函数
    • ret

3 | defer 函数的参数在语句出现时就已经确定了

  • go 语言是值传递(就是传递的参数,在新函数中会复制一份)
    • 因此 defer 语句出现时,其参数就复制了一份,后面对此参数的改变,不会影响 defer 已经复制的(除非传递的是指针)
func a() {
  i := 0
  defer fmt.Println(i) // 实际是 复制了一份 i,用于 defer 使用
  i++ // 不会影响 defer 复制的
  return
}

// 输出为: 0

4 | 运用学习

4.1 | 主函数拥有匿名返回值,返回字面量

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

// 输出为: 1
// return 直接返回 字面量 1,defer 函数无法操作

4.2 | 主函数拥有匿名返回值,返回变量

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

// 输出为:0

// 第二条规则,return:先设置返回值 --> 执行 defer --> 结束 ret
// 首先设置返回值,因为是匿名返回值,那我们可以随意起个名 annoy
// 因此首先设置返回值。  annoy = i
// 执行 defer。  i++ (不会对 annoy 产生影响)
// 返回。 返回 annoy

4.3 | 主函数拥有具名返回值

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

// 输出为:1

// 第二条规则,return:先设置返回值 --> 执行 defer --> 结束 ret
// 首先设置返回值,因为是匿名返回值,那我们可以随意起个名 annoy
// 因此首先设置返回值。  ret = 0
// 执行 defer。  ret++ (不会对 annoy 产生影响)
// 返回。 返回 ret

// 有人会奇怪,之前说的不是 『defer 会进行参数复制,不影响吗?』,怎么又影响了?
// 因为这里 defer 没有参数,直接易用外部函数的 ret 变量(这也叫做 『闭包』)

5 | defer 的底层原理了解

5.1 | defer 的创建与执行

源码包中 src/runtime/panic.go 定义了两个方法,分别用于创建 defer 和执行 defer

deferproc(): 负责把 defer 函数处理成 _defer 实例,并存入 goroutine 中的链表

deferreturn(): 负责把 defer 从 goroutine 链表中的 defer 实例取出并执行

简单理解为:
- 编译器在编译阶段把 defer 语句替换成 函数 deferproc()
- 在函数 return() 前插入了函数 deferreturn()
- 在运行时,每次执行 deferproc() 都创建一个运行时 _defer 实例并存储
- 函数返回前执行 deferreturn() 依次取出 _defer 实例并执行

5.2 | defer 的性能优化

  • heap-allocated:存储在堆上的 defer, Go 1.13 之前只有这种类型
  • stack-allocated:存储在站上的 defer, Go 1.13 引入这种类型
  • open-coded: 开放编码类型的 defer, Go 1.14 引入这种类型
  • 引入了新类型后也没有废弃原类型,而是多种类型并存
分析
  • heap-allocated
    • defer 节点存储在堆上
    • 痛点主要是频繁的堆内存分配及释放,性能稍差
  • stack-allocated
    • 编译器会直接在栈上预留 _defer 的存储空间,执行结束后不需要再释放内存
    • 由于栈空间有限,并不能把所有的 defer 都存储在栈中,所以还需要保留堆 defer
  • open-coded
    • 无论是堆 defer 还是 栈 defer,编译器都只能把 defer 替换成相应的函数(deferproc 堆的,deferproStack 栈的),然后创建 _defer 节点存储起来,最后通过 deferreturn() 函数取出这些 _defer 节点再执行
    • 开放编码的区别是:直接可以翻译成相应的执行代码并插入函数尾部,节省转换成 _defer 节点进行存储的代价
    • 但并不是所有的 defer 语句都可以被编译器处理成开放编码类型,如以下就不能:
      • 编译时禁用了编译器优化,即 -gcflags="-N -l"
      • defer 出现在循环语句中
      • 单个函数中 defer 出现了 8个以上,或者 return 语句的个数和 defer 语句的个数乘积超过了 15

6 | recover 函数一般配合来使用

  • recover() 函数成功处理异常后,无法再次回到本函数发生 panic 的位置继续执行

  • recover() 函数的调用必须要位于 defer 函数中,且不能出现在另一个嵌套函数中

  • recover() 函数可以消除本函数产生或收到的 panic,上游函数感知不到 panic 的发生

// recover() 位于 defer 中
func RecoverDemo1() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("A")
    }
  }()
  
  panic(nil)
  fmt.Println("B")
}

// recover() 函数必须直接位于 defer 函数中才有效,嵌套无效
func RecoverDemo1() {
  defer func() {
    func() { // recover 在 defer 的嵌套函数中 无效
         	if err := recover(); err != nil {
      		fmt.Println("A")
    	}
    }()
  }()
  
  panic(nil)
  fmt.Println("B")
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值