Go_defer详解

news2025/2/8 2:25:30

defer

1. 前言

defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。

为了方便描述,我们把创建defer的函数称为主函数,defer语句后面的函数称为延迟函数。

延迟函数可能有输入参数,这些参数可能来源于定义defer的函数,延迟函数也可能引用主函数用于返回的变量,也就是说延迟函数可能会影响主函数的一些行为,这些场景下,如果不了解defer的规则很容易出错。

其实官方说明的defer的三个原则很清楚,本节试图汇总defer的使用场景并做简单说明。

2. 热身

按照惯例,我们看几个有意思的题目,用于检验对defer的了解程度。

2.1 题目一

下面函数输出结果是什么?

func deferFuncParameter() {
    var aInt = 1

    defer fmt.Println(aInt)

    aInt = 2
    return
}

题目说明:
函数deferFuncParameter()定义一个整型变量并初始化为1,然后使用defer语句打印出变量值,最后修改变量值为2.

参考答案:
输出1。延迟函数fmt.Println(aInt)的参数在defer语句出现时就已经确定了,所以无论后面如何修改aInt变量都不会影响延迟函数。

2.2 题目二

下面程序输出什么?

package main

import "fmt"

func printArray(array *[3]int) {
    for i := range array {
        fmt.Println(array[i])
    }
}

func deferFuncParameter() {
    var aArray = [3]int{1, 2, 3}

    defer printArray(&aArray)

    aArray[0] = 10
    return
}

func main() {
    deferFuncParameter()
}

函数说明:
函数deferFuncParameter()定义一个数组,通过defer延迟函数printArray()的调用,最后修改数组第一个元素。printArray()函数接受数组的指针并把数组全部打印出来。

参考答案:
输出10、2、3三个值。延迟函数printArray()的参数在defer语句出现时就已经确定了,即数组的地址,由于延迟函数执行时机是在return语句之前,所以对数组的最终修改值会被打印出来。

2.3 题目三

下面函数输出什么?

func deferFuncReturn() (result int) {
    i := 1

    defer func() {
       result++
    }()

    return i
}

函数说明:
函数拥有一个具名返回值result,函数内部声明一个变量i,defer指定一个延迟函数,最后返回变量i。延迟函数中递增result。

参考答案:
函数输出2。函数的return语句并不是原子的,实际执行分为设置返回值–>ret,defer语句实际执行在返回前,即拥有defer的函数返回过程是:设置返回值–>执行defer–>ret。所以return语句先把result设置为i的值,即1,defer语句中又把result递增1,所以最终返回2。

3. defer规则

Golang官方博客里总结了defer的行为规则,只有三条,我们围绕这三条进行说明。

3.1 规则一:延迟函数的参数在defer语句出现时就已经确定下来了

官方给出一个例子,如下所示:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印”0”。

注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。

3.2 规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行

这个规则很好理解,定义defer类似于入栈操作,执行defer类似于出栈操作。

设计defer的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请A资源,再根据A资源申请B资源,根据B资源申请C资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把defer设计成LIFO的原因。

每申请到一个用完需要释放的资源时,立即定义一个defer来释放资源是个很好的习惯。

package main

import "fmt"
func deferList() {
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
}

func main() {
	deferList()
}
输出结果
3
2
1

3.3 规则三:延迟函数可能操作主函数的具名返回值

定义defer的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer所作用的函数,即延迟函数可能会影响到返回值。

若要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回的就足够了。

3.3.1 函数返回过程

有一个事实必须要了解,关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。

举个实际的例子进行说明这个过程:

func deferFuncReturn() (result int) {
    i := 1

    defer func() {
       result++
    }()

    return i
}

该函数的return语句可以拆分成下面两行:

result = i
return

而延迟函数的执行正是在return之前,即加入defer后的执行过程如下:

result = i
result++
return

所以上面函数实际返回i++值。

关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把return语句拆开都可以很好的理解,下面分别举例说明

3.3.2 主函数拥有匿名返回值,返回字面值

一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回”1”、”2”、”Hello”这样的值,这种情况下defer语句是无法操作返回值的。

一个返回字面值的函数,如下所示:

func foo() int {
    var i int

    defer func() {
        i++
    }()

    return 1
}

上面的return语句,直接把1写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。

3.3.3 主函数拥有匿名返回值,返回变量

一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。

一个返回本地变量的函数,如下所示:

func foo() int {
    var i int

    defer func() {
        i++
    }()

    return i
}

上面的函数,返回一个局部变量,同时defer函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为”anony”,上面的返回语句可以拆分成以下过程:

anony = i
i++
return

由于i是整型,会将值拷贝给anony,所以defer语句中修改i值,对函数返回值不造成影响。

3.3.4 主函数拥有具名返回值

主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。

一个影响函返回值的例子:

func foo() (ret int) {
    defer func() {
        ret++
    }()

    return 0
}

上面的函数拆解出来,如下所示:

ret = 0
ret++
return

函数真正返回前,在defer中对返回值做了+1操作,所以函数最终返回1。

4. defer实现原理

本节我们尝试了解一些defer的实现机制。

4.1 defer数据结构

源码包src/src/runtime/runtime2.go:_defer定义了defer的数据结构:

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

我们知道defer后面一定要接一个函数的,所以defer的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。

与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。

下图展示多个defer被链接的过程:

null

从上图可以看到,新声明的defer总是添加到链表头部。

函数返回前执行defer则是从链表首部依次取出执行,不再赘述。

一个goroutine可能连续调用多个函数,defer添加过程跟上述流程一致,进入函数时添加defer,离开函数时取出defer,所以即便调用多个函数,也总是能保证defer是按LIFO方式执行的。

4.2 defer的创建和执行

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

  • deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中;
  • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

可以简单这么理解,在编译阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

5. 总结

  • defer定义的延迟函数参数在defer语句出现时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)–>执行defer(若有)–>执行ret跳转
    声明defer处调用,其将defer函数存入goroutine的链表中;
  • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

可以简单这么理解,在编译阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1336249.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入了解队列:探索FIFO数据结构及队列

之前介绍了栈:探索栈数据结构:深入了解其实用与实现(c语言实现栈) 那就快马加鞭来进行队列内容的梳理。队列和栈有着截然不同的工作方式,队列遵循先进先出(FIFO)的原则,在许多场景下…

案例149:基于微信小程序的家庭财务管理系统的设计与实现

文末获取源码 开发语言:Java 框架:SSM JDK版本:JDK1.8 数据库:mysql 5.7 开发软件:eclipse/myeclipse/idea Maven包:Maven3.5.4 小程序框架:uniapp 小程序开发软件:HBuilder X 小程序…

怎么搭建实时渲染云传输服务器

实时渲染云传输技术方案,在数字孪生、虚拟仿真领域使用越来越多,可能很多想使用该技术方案项目还不知道具体该怎么搭建云传输服务器,具体怎么使用实时云渲染平台系统。点量云小芹将对这两个问题做集中分享。 一、实时渲染服务器怎么搭建&…

线性代数基础【3】向量

第一节 向量的概念与运算 一、基本概念 ①向量 ②向量的模(长度) ③向量的单位化 ④向量的三则运算 ⑤向量的内积 二、向量运算的性质 (一)向量三则运算的性质 α β β αα (β γ) (α β) γk (α β) kα kβ(k l) α kα lα (二)向量内积运…

C语言中函数调用和嵌套

函数是C语言的基本组成元素 函数调用 根据函数在程序中出现的位置有下列三种函数调用方式: 将函数作为表达式调用 将函数作为表达式调用时,函数的返回值参与表达式的运算,此时要求函数必须有返回值 int retmax(100,150); 将函数作为语句…

Jenkins——在流水线管道中使用指定的JDK

通过在tools下来指定JDK stage(Build) {tools {jdk "JDK8u231"}steps {sh /var/jenkins_home/tools/apache-maven-3.6.3/bin/mvn package} }JDK8u231是在全局配置下配置过的JDK

Stata回归结果该怎么分析呢?

回归分析是经典的数据分析方法之一,应用范围非常广泛,深受学者们的喜爱。它是研究分析某一变量受到其他变量影响的分析方法,基本思想是以被影响变量为因变量,以影响变量为自变量,研究因变量与自变量之间的因果关系。回…

中国信通院「星熠」案例公布,个推消息推送获评绿色SDK产品优秀案例

12月22日,由中国信息通信研究院安全研究所主办、大数据应用与安全创新实验室承办的“数据安全共同体计划成员大会(2023)”在京举行。每日互动(个推)作为“数据安全共同体计划”的联合发起单位及首批成员单位受邀出席大…

饥荒Mod 开发(二三):显示物品栏详细信息

饥荒Mod 开发(二二):显示物品信息 源码 前一篇介绍了如何获取 鼠标悬浮物品的信息,这一片介绍如何获取 物品栏的详细信息。 拦截 inventorybar 和 itemtile等设置字符串方法 在modmain.lua 文件中放入下面代码即可实现鼠标悬浮到 物品栏显示物品详细信…

一开始我还不信!高德导航红绿灯竟然能读秒?

高德导航红绿灯为啥能读秒? 1 内部员工吐露 每天工作其实就是负责自己片区的红绿灯,一大早就去校对时间,然后发布到后台。是的,统计出来的,而且还是人工统计,有误差请见谅[害羞] 真的是很辛苦了&#xf…

云HIS源码 云HIS解决方案 支持医保功能

云HIS系统重建统一的信息架构体系,重构管理服务流程,重造病人服务环境,向不同类型的医疗机构提供SaaS化HIS服务解决方案。 云HIS作为基于云计算的B/S构架的HIS系统,为基层医疗机构(包括诊所、社区卫生服务中心、乡镇卫…

element步骤条<el-steps>使用具名插槽自定义

element步骤条使用具名插槽自定义 步骤条使用具名插槽: <el-steps direction"vertical" :active"1"><el-step><template slot"description">//在此处可以写你的插槽内容</template>/el-step> </el-steps>步骤…

智能优化算法应用:基于鱼鹰算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于鱼鹰算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于鱼鹰算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.鱼鹰算法4.实验参数设定5.算法结果6.参考文献7.MA…

关于MULTI#STORM活动利用远程访问木马瞄准印度和美国的动态情报

一、基本内容 于2023年6月22日&#xff0c;一款代号为MULTI#STORM的新网络钓鱼活动将目标瞄准了印度和美国&#xff0c;利用JavaScript文件在受感染的系统上传播远程访问木马。 二、相关发声情况 Securonix的研究人员Den luzvyk、Tim Peck和Oleg Kolesnikov发表声明称&#x…

掌握JWT:解密身份验证和授权的关键技术

JSON Web Token 1、什么是JWT2、JWT解决了什么问题3、早期的SSO认证4、JWT认证5、JWT优势6、JWT结构Header 标头Payload 负载 Signature 签名 7、代码实现添加依赖生成Token认证token 8、工具类9、JWT整合Web10、拦截器校验11、网关路由校验12、解决多用户登录的问题13、客户端…

从流星雨启程:Python和Pygame下载与安装全过程

文章目录 一、前言二、下载安装过程1.官网下载安装包2.安装python过程第一步第二步第三步第四步第五步安装完成 3.简单测试Python3.1 检查 Python 版本号3.2 打开 Python 解释器3.3 输入你的第一个代码3.4 运行 Python 脚本 4.安装Pygame4.1 cmd命令安装Pygame4.2 pip升级4.3 安…

论文阅读——X-Decoder

Generalized Decoding for Pixel, Image, and Language Towards a Generalized Multi-Modal Foundation Model 1、概述 X-Decoder没有为视觉和VL任务开发统一的接口&#xff0c;而是建立了一个通用的解码范式&#xff0c;该范式可以通过采用共同的&#xff08;例如语义&#…

12月25日作业

串口发送控制命令&#xff0c;实现一些外设LED 风扇 uart4.c #include "uart4.h"void uart4_config() {//1.使能GPIOB\GPIOG\UART4外设时钟RCC->MP_AHB4ENSETR | (0x1 << 1);RCC->MP_AHB4ENSETR | (0x1 << 6);RCC->MP_APB1ENSETR | (0x1 <…

博易大师智星系统外盘资管系统的功能介绍!

1. 市场行情数据接收和显示&#xff1a;软件需要接收实时的市场行情数据&#xff0c;并将其以图形或数字的形式显示出来&#xff0c;包括价格、成交量、成交额等信息。 2. 交易操作界面&#xff1a;软件需要提供一个交易操作界面&#xff0c;供用户进行交易操作&#xff0c;包括…

HarmonyOS共享包HAR

共享包概述 OpenHarmony提供了两种共享包&#xff0c;HAR&#xff08;Harmony Archive&#xff09;静态共享包&#xff0c;和HSP&#xff08;Harmony Shared Package&#xff09;动态共享包。 HAR与HSP都是为了实现代码和资源的共享&#xff0c;都可以包含代码、C库、资源和配…