Go语言之defer(原理、常见的坑)

一、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进行判断,若获取资源失败,就无需进行资源释放,避免了没获取到资源而执行释放资源函数产生错误。

你可能感兴趣的:(golang,golang)