Go语言之延迟调用函数defer

2023-10-30 15:17:41 浏览数 (1)

写在前面的话:

在接触defer之后,觉得Go的这一特性很好,有点类似于C 的析构函数,不过它们却有很大的不同。主要的区别点是defer实现在函数里面,作用域也是在函数里面,当函数的return语句被调用之后,才会调用到这个defer声明的函数。而析构函数实现在类里面,作用域是在类内部,在该类的实例被销毁的时候,就会被调用到。

在谈论defer之前,笔者问了自己三个问题: 为什么我们需要defer? 如何才能更好的使用它? defer是如何实现的?

基于上面的三个问题,笔者做了简单的整理。

一.为什么我们需要defer

我们在写程序的时候,往往会碰到下面的两种情况。

第一种释放资源,当我们在创建一个资源的时候,往往需要释放资源,但是因为逻辑分支太多的缘故,我们要在每一个异常分支里面去实现释放资源的 操作。这样以来的话,就存在两个问题,第一,我们需要散弹式修改,释放资源的地方很多,每个都要填写上面,代码不容易维护。第二,异常分支太多的话,很容易漏掉,或者提前return了,进而导致资源没有释放掉,这样会产生代码漏洞。

第二种处理异常,代码实现里面,有一些异常是可以从逻辑代码里面控制的,有一些却未必容易控制,特别是一些很难捕捉到到异常,这种主要来源于操作系统内核或者硬件提示的异常信息。

1.C 里面这两种情况,都有对应的处理方法,第一种采用析构函数去释放这些资源,第二种情况采用try-catch的方式去捕获和处理这些异常(备注:这部分内容会专门整理一篇文章介绍)。

2.到了Go之后,我发现C 的这两种实现方式都不存在了,那怎么办呢?于是defer产生了,这种在普通函数的return之后会调用的延迟调用函数,该发挥作用了。

二.defer的使用规则

defer函数调用时间,发生在该函数的return之后,主要用三种使用规则,说是三种规则,其实更像是三种注意事项。

1)当defer被声明时,其参数就会被实时解析。

代码语言:javascript复制
package main
import (
"fmt"
)

func main() {
  var i int = 1
  // i的值在defer第一次走到的位置就被确认下来了
  defer fmt.Println("defer i value:", i) 
  i  
  fmt.Println("Main i value:", i)
}

output:

代码语言:javascript复制
Main i value: 2
defer i value: 1

备注:对于指针来说,这个参数是地址,指针指向的数据还是有可能会被更改的。

2)当一个函数中有多个defer函数时,它们的执行顺序是先进后出。

这种处理场景,一般是有几个资源,而这些资源之间是有依赖关系的。

代码语言:javascript复制
package main
import (
"fmt"
)

func main() {
  var i int = 1
  defer fmt.Println("defer i value:", i) // 3rd called
  i  
  defer fmt.Println("defer i value:", i) // 2nd called
  i  
  defer fmt.Println("defer i value:", i) // 1st called
  i  
  fmt.Println("Main i value:", i)
}

OutPut:

代码语言:javascript复制
Main i value: 4
defer i value: 3
defer i value: 2
defer i value: 1

3)defer可以读取有名返回值。

关于这一个规则,笔者觉得这是defer的一个副作用,毕竟返回值在return之后,是不希望被改掉的。不过也有好处,就是一旦希望对函数的返回值做一些特殊操作的时候,例如希望将返回值占内存很大的内容写到文件里或者内存里。

代码语言:javascript复制
package main

import (
  "fmt"
)

func deferFunc() (i int ) {
  return 1 // 返回值1
}

func deferFunc0() (i int ) {
  defer func() { i   }() // 会更改i的值,但是没有办法影响返回值
  return 1  // 返回值 直接将1这种数字写回了栈中,并直接返回了
}

func deferFunc1() (i int ) {
  defer func() { i   }()// step2: 会继续操作i  
  return i // step1: 返回值会复制给一个临时变量,再返回出去
}

func main() {
  i := 0
  i = deferFunc()
  fmt.Println("Main  i value:", i)
  i = deferFunc0()
  fmt.Println("Main0 i value:", i)
  i = deferFunc1()
  fmt.Println("Main1 i value:", i)
  i  
  fmt.Println("Main2 i value:", i)
}

Output:

代码语言:javascript复制
Main  i value: 1
Main0 i value: 2
Main1 i value: 1
Main2 i value: 2
三.defer的实现原理

1)defer 的数据结构

代码语言:javascript复制
type _defer struct{ 
  spuintptr//函数栈指针 
  pcuintptr//程序计数器
  fn*funcval//函数地址
  link*_defer//指向自身结构的指针,用于链接多个defer
}

图片发自简书App

每次声明一个defer函数都会从链表头部开始插入。

函数返回前执行defer是从链表首部一次取出执行。

2)defer的创建与执行

deferproc():在声明defer处调用,将其defer函数存入goroutine的链表中。

deferreturn():在ret指令前调用,将defer从对应的链表中取出并执行。

过程如下:在编译阶段声明defer处插入函数deferproc() ,在函数return前插入函数deferreturn()。

参考资料:Go语言程序设计

https://studygolang.com/articles/16067

灰子作于二零一九年三月十九日。

0 人点赞