Go语言之defer(原理、常见的坑)
作者:mmseoamin日期:2023-12-21

一、defer介绍

  • 简单来说,Go中特有的defer关键字,本质就是延迟自动执行函数。
  • 例1:
    package main
    import "fmt"
    func df()int{
    	i:=5
    	defer func(){
    		i = i + 10
    		fmt.Println("defer函数中的i:",i)
    	}()
    	fmt.Println("df中的i:",i)
    	return i
    }
    func main(){
    	ri := df()
    	fmt.Println(ri)
    }
    
    结果:Go语言之defer(原理、常见的坑),在这里插入图片描述,第1张
  • 由上述代码结果不难发现,虽然“fmt.Println(“defer函数中的i:”,i)”这行代码在“fmt.Println(“df中的i:”,i)”这行代码之前,但是,先执行打印“df中的i”再执行打印“defer函数中的i”。且最终df函数的返回值是5而不是15。
  • 例2:
    package main
    import "fmt"
    func df()int{
    	i:=5
    	defer func(j int){
    		j = j + 10
    		fmt.Println("defer函数中的j:",j)
    	}(i + 1)
    	
    	i=i+30
    	return i
    }
    func main(){
    	ri := df()
    	fmt.Println(ri)
    }
    
    结果:

    Go语言之defer(原理、常见的坑),在这里插入图片描述,第2张

  • 由上述代码不难发现,传入到defer后面函数的形参j的实参i+1不是35,而是6。
  • 例3:
    package main
    import "fmt"
    func df()int{
    	i:=5
    	defer func(j int){
    		j = j + 10
    		fmt.Println("defer函数1中的j:",j)
    	}(i + 1)
    	defer func(j int){
    		j = j + 20
    		fmt.Println("defer函数2中的j:",j)
    	}(i + 1)
    	defer func(j int){
    		j = j + 30
    		fmt.Println("defer函数3中的j:",j)
    	}(i + 1)
    	return i
    }
    func main(){
    	ri := df()
    	fmt.Println(ri)
    }
    
    结果:

    Go语言之defer(原理、常见的坑),在这里插入图片描述,第3张

  • 由上述代码不难发现,第三个defer函数先执行,然后再是第二个defer函数执行,最后再是第一个defer函数执行。

    1.defer关键字的特性:

    (1)延迟执行:defer关键字后的函数都是在整个函数执行结束return之后才执行的。正如上述例1代码中最终df函数的返回值是5而不是15,且先执行打印“df中的i”再执行打印“defer函数中的i”所示。上述代码是先执行完df函数中除了defer后面的函数之外的语句。return i(i的值依旧是5)之后,再执行defer后面的函数,执行i = i +10,且打印i

    (2)参数预计算:defer函数的形参会在定义时就完成了该参数的拷贝。正如上述例2代码中传入到defer后面函数的形参j的实参i+1不是35,而是6。

    (3)FILO:先进后出,若多个defer函数在同一函数内,执行顺序遵循先进后出原理。即第一个defer函数最后一个被执行。正如上述例3代码中第三个defer函数先执行,然后再是第二个defer函数执行,最后再是第一个defer函数执行

    二、defer原理

    1.defer原理
    • defer的数据结构:
      type _defer struct{
      	sp    uintptr   //函数栈指针
      	pc    uintptr   //程序计数器
      	fn    *funcval  //函数地址
      	lnk   *_defer   //指向自身结构的指针,用于链接多个defer
      }
      

      Go语言之defer(原理、常见的坑),在这里插入图片描述,第4张

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

        (1)defer的创建deferproc():在defer的声明处调用,其将defer函数存于goroutine的链表中

        (2)defer的执行deferreturn():在return指令前调用,其将defer函数从链表中取出并执行

        (3)可以简单理解为:声明defer处插入了deferproc()函数,在函数return前插入了deferreturn()函数

        2.defer的三种机制

        (1) 堆上分配
        • 在Go1.13之前都是采用堆上分配的,其创建原理就是直接在堆上申请内存,再将该defer结构体放到当前goroutine协程的_defer链表上的。
        • 申请堆内存的时候,是有缓存池的设计的,每个逻辑处理器都有一个局部缓存池,全局有一个全局缓存池,每次都是从局部缓存池获取对象。

          (1.1)当defer执行完毕,会放入到局部缓存池

          (1.2)当局部缓存池容纳足够的对象时,会放到全局缓存池

          (1.3)当逻辑处理器的局部缓存池为空时,会从全局缓存池中取一部分放到局部缓存池

          (1.4)当对象没有被使用时,会被垃圾回收

        • 调用时直接遍历_defer链表从链表头开始执行,执行时,需要当前defer相关的参数和函数都重新放入栈中,这个会带来额外的开销。
        • 堆上分配采用deferproc()函数
          (2) 栈上分配
          • Go1.13为了解决堆分配的效率问题,对于最多只调用一次的defer采用了在栈上分配的策略
          • 和堆上分配相比,栈分配第一阶段采用了deferprocStack()函数
          • 在栈上分配defer的好处在于函数返回后_defer便释放,不需要考虑内存分配时产生的性能开销,只需维护_defer链表即可。
            (3) 内联优化
            • 虽然栈上分配已经大大减少了调用耗时,但是和直接调用函数相比,还是差很多。所以内联优化就是将defer函数直接内联到代码中。
            • 内联优化只有在满足以下条件时才会启用:

              (3.1)函数的defer数量小于等于8个

              (3.2)函数的defer关键字不能在循环中使用

              (3.3)函数的return语句和defer语句的数量的乘积小于等于15个

              三、defer常见的坑

              1.defer和return的执行顺序

              package main
              import "fmt"
              func df()int{
              	i:=5
              	defer func(){
              		i = i + 10
              	}()
              	return i
              }
              func main(){
              	ri := df()
              	fmt.Println(ri)
              }
              
              • defer函数执行是在函数return之前,这句话很多人会误以为,上述代码结果是15,实际不对,上述代码结果是5。这个是为什么呢?
              • 因为defer确实在return之前调用,但是return i语句并不是一条原子指令,它分为两步:1.将返回值放到一个临时变量中(为返回值赋值),,2.执行ret指令将返回值return到被调用处。而defer语句是在第一步和第二步之间执行的。故上述代码执行顺序是:1.给返回值i赋值,2.执行defer函数,3.return到函数调用处。
                (1) 无名返回值(函数返回值没有命名的返回值)
                package main
                import "fmt"
                func df()int{
                	i:=5
                	defer func(){
                		i = i + 10
                		fmt.Println("defer函数中的i:",i)
                	}()
                	fmt.Println("df中的i:",i)
                	return i
                }
                func main(){
                	ri := df()
                	fmt.Println(ri)
                }
                

                结果:Go语言之defer(原理、常见的坑),在这里插入图片描述,第5张

                • 对于无名返回值,在return之前会随机生成一个临时零值(假设为j)作为返回值,然后将i赋值给j,后续defer函数内是对i进行操作的,并不会影响到j。
                  (2) 有名返回值(函数返回值是已经命名的返回值)
                  package main
                  import "fmt"
                  func df()(i int){
                  	i = 5
                  	defer func(){
                  		i = i + 10
                  		fmt.Println("defer函数中的i:",i)
                  	}()
                  	fmt.Println("df中的i:",i)
                  	return i
                  }
                  func main(){
                  	ri := df()
                  	fmt.Println(ri)
                  }
                  

                  结果:Go语言之defer(原理、常见的坑),在这里插入图片描述,第6张

                  • 对于有名返回值,返回值已经提前定义了,不会产生临时零值的。相当于函数返回值i,后续defer函数中对i进行操作会影响i。

                    2.defer需要定义在panic之前

                    • 若defer定义在panic之后会直接panic,并不会执行defer函数
                      package main
                      import "fmt"
                      func main(){
                      	panic("aaaaa")
                      	defer func(){
                      		fmt.Println("执行defer")
                      	}()
                      }
                      
                      结果:

                      Go语言之defer(原理、常见的坑),在这里插入图片描述,第7张

                    • 故需要defer定义在panic之前,才会先执行defer函数然后panic
                      package main
                      import "fmt"
                      func main(){
                      	defer func(){
                      		fmt.Println("执行defer")
                      	}()
                      	panic("aaaaa")
                      }
                      
                      结果:

                      Go语言之defer(原理、常见的坑),在这里插入图片描述,第8张

                      3.先判断err,再defer释放资源

                      • 获取一些资源时会出现err,若我们需要进行defer释放资源时,需要先对err进行判断,若获取资源失败,就无需进行资源释放,避免了没获取到资源而执行释放资源函数产生错误。