go defer 解析

293 阅读4分钟

defer使用场景

先来看一段代码:

func multiplyClose() {
   f, err := os.Open("./deferStudy")
    if err!=nil {
        f.Close() 
        log.Fatalf("Fail to open file : %v",err)
    }
    //....
    //....
    f.Close() 
}

无论是否正常退出或者panic退出,我们都需要执行某些特定操作的时候,这个时候defer就起到了至关重要的作用。上面代码中调用了两次Close方法,而目的是一致,即在函数结束时执行一次Close。若代码中仍需要进行多次关键错误判断,多次的使用f.Close()只会造成代码的冗余。这时候我们的主角defer 登场了. ✌️

//延迟对Close这样的函数的调用有两个优点。
//首先,它保证您永远不会忘记关闭文件,如果稍后编辑该函数以添加新的返回路径,则很容易犯这个错误。
//其次,它意味着close位于open附近,这比把它放在函数的末尾要清楚得多。
func multiplyClose() {
   f, err := os.Open("./deferStudy")
   defer f.Close()
   if err!=nil {
      //f.Close()
      log.Fatalf("Fail to open file : %v",err)
   }
   //....
   //....
}

defer语句经常被⽤于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执⾏路径下,资源被释放.

defer机制

defer 后面参数应该是一个函数调用(也可以是方法,是方法的话要包含接收器receiver) 后续我会写一篇关于方法的文章,欢迎支持🥰

func main(){
   var i int
   defer i=1 //error:Expression in defer must be a function call
}

语义化的去理解它,defer后面应该跟着的是重要的”行为",defer to do Something that has to be done,去做一些我们必须做的。函数在go里可是一等公民💪。不要忘记写小括号!!

defer 的调用顺序采用了“后进先出”的方式,先声明的后调用。

func asStack() {
   defer func() {
      fmt.Println("3")
   }()
   defer func() {
      fmt.Println("2")
   }()
   defer func() {
      fmt.Println("1")
   }()
} 
输出:
    1
    2
    3

defer后的函数的参数在定义时就已经决定了。放上一句go官方的解释”The arguments to the deferred function (which include the receiver if the function is a method) are evaluated when the defer executes, not when the call executes."

func evaluated() {
   for i := 1; i <= 5; i++ {
      defer fmt.Print(i) //若不是在defer executes时期决定,结果将输出55555
   }
}
输出:54321

再加一个例子:

func evaluatedArg() {
   i := 1
   defer func(x int) {
      fmt.Printf("in defer:%v\n", x)
      fmt.Printf("in defer:%v\n", i) 
   }(i)
   i = 2
   defer fmt.Printf("out:%v\n", i)
   //i=3 加入改行in defer 中的i会输出为3
}
输出:
out:2
in defer:1
in defer:2

第一个defer传入参数 i,这时 i 的值为1,即 x 已被确定。后续 i 值变为2,第二个defer紧跟着的Printf函数中的 i 在定下来时为2,所以输出2。defer executes时期只能确定参数,不能确定函数内的情况,所以in defer中的i只有在调用时才能确定。为什么会这样呢?🤔

其实defer指令会创建一个叫做_defer的结构体,该结构体内的信息告诉编译器该怎么执行当前defer。

defer原理.png

type _defer struct {
	siz     int32 // 参数和返回值共占用空间大小,这段空间会在_defer结构体后面,
                      //用于defer注册时候保存参数,并在执行时候拷贝到调用者参数与返回值空间。
	started bool // 标记defer是否已经执行
	heap    bool // 标记该_defer结构体是否分配在堆上

	openDefer bool // 标志是否使用open coded defer方式处理defer
	sp        uintptr  // 调用者栈指针,执行时会根据sp判断该defer是否是当前执行调用者注册的
	pc        uintptr  // deferprocStack或deferproc的返回地址
	fn        *funcval // defer函数,是funcval类型
	_panic    *_panic  // panic链表,用于panic处理
	link      *_defer // 链接到下一个_defer结构体,即该在_defer之前注册的_defer结构体

	fd   unsafe.Pointer // funcdata for the function associated with the frame
	varp uintptr        // value of varp for the stack frame
	framepc uintptr
}

defer语句会先申请获取一片空间用于存放_defer结构体本身以及结构体所需内容的空间。如果每次都申请新的_defer,那么消耗的空间自然会非常多,这时缓存机制就起到了很好的作用。_defer会使用之前使用完的_defer的空间,若无旧_defer可用则申请新的_defer (推荐一篇深入理解defer的文章)

我们先看siz字段,其在注册时确定了参数和返回值空间大小(后面我称它为a空间),并将参数保存到a空间中,a空间紧跟在defer结构体之后的,这就是参数在executes defer时期就确定的原因。

接着我们看fn字段 fn指针指向一个funcval类型,funcval结构体内有一个fn字段,指向defer函数入口地址。

使用defer时要注意的点

defer的调用有可能会影响return的返回值

//function 1 提前声明返回值
func returnEff() (i int) {
   defer func() {
      i++
      fmt.Printf("returnEff: %v\n", i)
   }()
   return

}

func main() {
   fmt.Printf("returnResult: %v\n", returnEff())
}
输出:
returnEff: 1
returnResult: 1

//function 2
func returnEff() int {
   var i int
   defer func() {
      i++
      fmt.Printf("returnEff: %v\n", i)
   }()
   return i

}

func main() {
   fmt.Printf("returnResult:%v\n", returnEff())
}
输出:
returnEff: 1
returnResult: 0

为什么会这样呢?🤔 其实return并非一个原子操作,分为返回值赋值和RET指令两步。如果是匿名返回值,则在返回时会申请一个匿名变量,返回值会赋给这个匿名变量,函数返回这个匿名变量,所以在匿名返回值的情况下,defer并不会影响到返回值,因为根本就没途径可以获取到匿名变量(函数内根本不知道匿名变量放在哪个地址上)。但是如果是具名返回值,返回值在函数声明时就已经定义了(可以知道返回值地址),返回的变量是能在defer中访问到,而且RET指令执行是在runtime.deferreturn之后的。所以在具名返回值的情况下,defer中对返回值的修改会影响到return的RET操作。

defer应该避免放在loop里

func returnEff() error{

   var filenames []string
   for _, filename := range filenames {
      f, err := os.Open(filename)
      if err != nil {
         return err
      }
      defer f.Close() // Possible resource leak, 'defer' is called in the 'for' loop
   }
   return nil
}

在IDE中编写代码的时候,其实IDE就已经警告⚠我defer放在了loop中。我们可以设想一下如果filenames中有上万个数据会怎样,defer是在函数结束时执行的,其绑定的是函数而不是代码块。这里就相当于建立了数个open file连接,但连接只有在函数结束时才能close。建立多个连接却不中断,这会导致系统崩溃。

For programmers accustomed to block-level resource management from other languages, defer may seem peculiar, but its most interesting and powerful applications come precisely from the fact that it's not block-based but function-based. In the section on panic and recover we'll see another example of its possibilities。——Go官方

总结一下:

  • defer 后面的参数应该是一个函数调用或者方法调用,是方法的话就要带上接收器。
  • defer 结构体采用链表的形式连接,从表头插入,从表头取出,表现为“后进先出”,而结构体_defer在申请空间前,会先查看_defer缓冲池有无缓存的_defer,没有可用_defer才会申请新空间。
  • defer 函数的参数在注册时期就已经决定了。
  • defer 应该避免放在Loop循环中。

参考:

深入go语言之旅 推荐⭐️⭐️⭐️⭐️

go官方的Effective Go 强烈推荐⭐️⭐️⭐️⭐️⭐️

笔者只是一个普通人,文章内容不一定是100%正确的,文章只是我的个人理解。我希望大家看完这篇文章之后,能够去翻阅更多的资料,如果有不同的看法,欢迎指出!😄