Go语言之defer(原理、常见的坑)
创始人
2024-06-02 18:27:53
0

一、defer介绍

  • 简单来说,Go中特有的defer关键字,本质就是延迟自动执行函数。
  • 例1:
    package mainimport "fmt"func df()int{i:=5defer func(){i = i + 10fmt.Println("defer函数中的i:",i)}()fmt.Println("df中的i:",i)return i
    }
    func main(){ri := df()fmt.Println(ri)
    }
    
    结果:在这里插入图片描述
  • 由上述代码结果不难发现,虽然“fmt.Println(“defer函数中的i:”,i)”这行代码在“fmt.Println(“df中的i:”,i)”这行代码之前,但是,先执行打印“df中的i”再执行打印“defer函数中的i”。且最终df函数的返回值是5而不是15。
  • 例2:
    package mainimport "fmt"func df()int{i:=5defer func(j int){j = j + 10fmt.Println("defer函数中的j:",j)}(i + 1)i=i+30return i
    }
    func main(){ri := df()fmt.Println(ri)
    }
    
    结果:
    在这里插入图片描述
  • 由上述代码不难发现,传入到defer后面函数的形参j的实参i+1不是35,而是6。
  • 例3:
    package mainimport "fmt"func df()int{i:=5defer func(j int){j = j + 10fmt.Println("defer函数1中的j:",j)}(i + 1)defer func(j int){j = j + 20fmt.Println("defer函数2中的j:",j)}(i + 1)defer func(j int){j = j + 30fmt.Println("defer函数3中的j:",j)}(i + 1)return i
    }
    func main(){ri := df()fmt.Println(ri)
    }
    
    结果:
    在这里插入图片描述
  • 由上述代码不难发现,第三个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
    }
    

在这里插入图片描述

  • 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 mainimport "fmt"func df()int{i:=5defer 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 mainimport "fmt"func df()int{i:=5defer func(){i = i + 10fmt.Println("defer函数中的i:",i)}()fmt.Println("df中的i:",i)return i
}
func main(){ri := df()fmt.Println(ri)
}

结果:在这里插入图片描述

  • 对于无名返回值,在return之前会随机生成一个临时零值(假设为j)作为返回值,然后将i赋值给j,后续defer函数内是对i进行操作的,并不会影响到j

(2) 有名返回值(函数返回值是已经命名的返回值)

package mainimport "fmt"func df()(i int){i = 5defer func(){i = i + 10fmt.Println("defer函数中的i:",i)}()fmt.Println("df中的i:",i)return i
}
func main(){ri := df()fmt.Println(ri)
}

结果:在这里插入图片描述

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

2.defer需要定义在panic之前

  • 若defer定义在panic之后会直接panic,并不会执行defer函数
    package mainimport "fmt"func main(){panic("aaaaa")defer func(){fmt.Println("执行defer")}()
    }
    
    结果:
    在这里插入图片描述
  • 故需要defer定义在panic之前,才会先执行defer函数然后panic
    package mainimport "fmt"func main(){defer func(){fmt.Println("执行defer")}()panic("aaaaa")
    }
    
    结果:
    在这里插入图片描述

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

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

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...