Golang Defer 执行顺序

Golang Defer 执行顺序

1. defer的执行顺序

多个defer出现的时候,它是一个“栈”的关系,也就是先进后出。一个函数中,写在前面的defer会比写在后面的defer调用的晚。

代码示例:

func TestDefer01(t *testing.T) {defer func1()defer func2()defer func3()
}func func1() {fmt.Println("A")
}func func2() {fmt.Println("B")
}func func3() {fmt.Println("C")
}

输出

=== RUN   TestDefer01
C
B
A
--- PASS: TestDefer01 (0.00s)

2. defer与return谁先谁后

结论为:return之后的语句先执行,defer后的语句后执行

func func1() {fmt.Println("A")
}func func2() {fmt.Println("B")
}func func3() {fmt.Println("C")
}func deferFunc() int {fmt.Println("defer func called")return 0
}func returnFunc() int {fmt.Println("return func called")return 0
}func returnAndDefer() int {defer deferFunc()return returnFunc()
}func TestDeferReturn01(t *testing.T) {returnAndDefer()
}

输出

=== RUN   TestDeferReturn01
return func called
defer func called
--- PASS: TestDeferReturn01 (0.00s)

3. 函数的返回值初始化

该知识点不属于defer本身,但是调用的场景却与defer有联系,所以也算是defer必备了解的知识点之一。

如 : func DeferFunc1(i int) (t int) {}
其中返回值 t int ,这个 t 会在函数起始处被初始化为对应类型的零值并且作用域为整个函数。

代码示例

func DeferFunc11(i int) (t int) {fmt.Println("t = ", t)return 2
}func TestReturnInit(t *testing.T) {DeferFunc11(10)
}

输出

=== RUN   TestReturnInit
t =  0
--- PASS: TestReturnInit (0.00s)

4. 有名函数返回值遇见defer情况

在没有defer的情况下,其实函数的返回就是与return一致的,但是有了defer就不一样了。

我们通过知识点2得知,先return,再defer,所以在执行完return之后,还要再执行defer里的语句,依然可以修改本应该返回的结果。

代码示例

func returnButDefer() (t int) { //t初始化0, 并且作用域为该函数全域defer func() {t = t * 10}()return 1
}func TestReturnButDefer(t *testing.T) {println(returnButDefer())
}

输出

=== RUN   TestReturnButDefer
10
--- PASS: TestReturnButDefer (0.00s)

代码分析

returnButDefer() 本应的返回值是 1 ,但是在return之后,又被defer的匿名func函数执行,所以 t=t*10 被执行,最后 returnButDefer() 返回给上层 main() 的结果为10

5. defer遇见panic

我们知道,能够触发defer的是遇见return(或函数体到末尾)和遇见panic。

遇到panic时,遍历本协程的defer链表,并执行defer。在执行defer过程中:遇到recover则停止panic,返回recover处继续往下执行。如果没有遇到recover,遍历完本协程的defer链表后,向stderr抛出panic信息。

defer遇见panic,执行顺序

  1. 遇见panic,强行defer出栈,并且每个defer均受到异常,不捕获则转移到下一个
  2. 直到defer捕获异常,或者上层显示异常

defer 最大的功能是 panic 后依然有效
所以defer可以保证你的一些资源一定会被关闭,从而避免一些异常出现的问题。

defer遇见panic,但是并不捕获异常的情况

代码示例

func TestDeferPanic01(t *testing.T) {defer_call()fmt.Println("main 正常结束")
}func defer_call() {defer func() { fmt.Println("defer: panic 之前1") }()defer func() { fmt.Println("defer: panic 之前2") }()panic("异常内容") //触发defer出栈defer func() { fmt.Println("defer: panic 之后,永远执行不到") }()
}

输出

=== RUN   TestDeferPanic01
defer: panic 之前2
defer: panic 之前1
--- FAIL: TestDeferPanic01 (0.00s)
panic: 异常内容 [recovered]panic: 异常内容
...
...
...

defer遇见panic,并捕获异常

代码示例

func deferCall02() {defer func() {fmt.Println("defer: panic 之前1, 捕获异常")if err := recover(); err != nil {fmt.Println(err)}}()defer func() { fmt.Println("defer: panic 之前2, 不捕获") }()panic("异常内容") //触发defer出栈defer func() { fmt.Println("defer: panic 之后, 永远执行不到") }()
}func TestDeferPanic02(t *testing.T) {deferCall02()fmt.Println("main 正常结束")
}

输出

=== RUN   TestDeferPanic02
defer: panic 之前2, 不捕获
defer: panic 之前1, 捕获异常
异常内容
main 正常结束
--- PASS: TestDeferPanic02 (0.00s)

6. defer中包含panic

代码示例

func TestDeferPanic03(t *testing.T) {defer func() {if err := recover(); err != nil {fmt.Println(err)} else {fmt.Println("fatal")}}()defer func() {panic("defer panic")}()panic("panic")
}

输出

=== RUN   TestDeferPanic03
defer panic
--- PASS: TestDeferPanic03 (0.00s)

分析

panic仅有最后一个可以被revover捕获。

触发 panic("panic") 后defer顺序出栈执行,第一个被执行的defer中 会有 panic("defer panic") 异常语句,这个异常将会覆盖掉main中的异常 panic("panic") ,最后这个异常被第二个执行的defer捕获到。

7. defer下的函数参数包含子函数

代码示例

func deferPanic04Func(index int, value int) int {fmt.Println("DeferPanic04Func", index)return index
}func TestDeferPanic04(t *testing.T) {defer deferPanic04Func(1, deferPanic04Func(3, 0))defer deferPanic04Func(2, deferPanic04Func(4, 0))
}

输出

=== RUN   TestDeferPanic04
DeferPanic04Func 3
DeferPanic04Func 4
DeferPanic04Func 2
DeferPanic04Func 1
--- PASS: TestDeferPanic04 (0.00s)

代码分析

这里,有4个函数,他们的index序号分别为1,2,3,4。

那么这4个函数的先后执行顺序是什么呢?这里面有两个defer, 所以defer一共会压栈两次,先进栈1,后进栈2。 那么在压栈function1的时候,需要连同函数地址、函数形参一同进栈,那么为了得到function1的第二个参数的结果,所以就需要先执行function3将第二个参数算出,那么function3就被第一个执行。同理压栈function2,就需要执行function4算出function2第二个参数的值。然后函数结束,先出栈fuction2、再出栈function1.

所以顺序如下:

  1. ● defer压栈function1,压栈函数地址、形参1、形参2(调用function3) --> 打印3
  2. ● defer压栈function2,压栈函数地址、形参1、形参2(调用function4) --> 打印4
  3. ● defer出栈function2, 调用function2 --> 打印2
  4. ● defer出栈function1, 调用function1–> 打印1

8. defer面试真题

func DeferFunc1(i int) (t int) {t = idefer func() {t += 3}()return t
}func DeferFunc2(i int) int {t := idefer func() {t += 3}()return t
}func DeferFunc3(i int) (t int) {defer func() {t += i}()return 2
}func DeferFunc4() (t int) {defer func(i int) {fmt.Println("DeferFunc4 i=", i)fmt.Println("DeferFunc4 t=", t)}(t)println("DeferFunc4 , func start .")t = 1println("DeferFunc4 , func end .")return 2
}func TestQue01(t *testing.T) {fmt.Println("DeferFunc1 =", DeferFunc1(1))fmt.Println("DeferFunc2 =", DeferFunc2(1))fmt.Println("DeferFunc3 =", DeferFunc3(1))DeferFunc4()
}

输出

=== RUN   TestQue01
DeferFunc1 = 4
DeferFunc2 = 1
DeferFunc3 = 3
DeferFunc4 , func start .
DeferFunc4 , func end .
DeferFunc4 i= 0
DeferFunc4 t= 2
--- PASS: TestQue01 (0.00s)

9. 真题分析

DeferFunc1

func DeferFunc1(i int) (t int) {t = idefer func() {t += 3}()return t
}
  1. 将返回值t赋值为传入的i,此时t为1
  2. 执行return语句将t赋值给t(等于啥也没做)
  3. 执行defer方法,将 t + 3 = 4
  4. 函数返回 4

因为t的作用域为整个函数所以修改有效。

DeferFunc2

func DeferFunc2(i int) int {t := idefer func() {t += 3}()return t
}
  1. 创建变量t并赋值为1
  2. 执行return语句,注意这里是将t赋值给返回值,此时返回值为1(这个返回值并不是t)
  3. 执行defer方法,将 t + 3 = 4
  4. 函数返回返回值 1

也可以按照如下代码理解

func DeferFunc2(i int) (result int) {t := idefer func() {t += 3}()return t
}

上面的代码return的时候相当于将t赋值给了result,当defer修改了t的值之后,对result是不会造成影响的。

DeferFunc3

func DeferFunc3(i int) (t int) {defer func() {t += i}()return 2
}
  1. 首先执行 return 将返回值t赋值为 2
  2. 执行defer方法将 t + 1
  3. 最后返回 3

DeferFunc4

func DeferFunc4() (t int) {defer func(i int) {fmt.Println("DeferFunc4 i=", i)fmt.Println("DeferFunc4 t=", t)}(t)println("DeferFunc4 , func start .")t = 1println("DeferFunc4 , func end .")return 2
}

输出顺序

DeferFunc4 , func start .
DeferFunc4 , func end .
DeferFunc4 i= 0
DeferFunc4 t= 2
  1. 初始化返回值t为零值 0
  2. 首先执行defer的第一步,赋值defer中的func入参t为0
  3. 执行defer的第二步,将defer压栈
  4. 将t赋值为1
  5. 执行return语句,将返回值t赋值为2
  6. 执行defer的第三步,出栈并执行

因为在入栈时defer执行的func的入参已经赋值了,此时它作为的是一个形式参数,所以打印为0;相对应的因为最后已经将t的值修改为2,所以再打印一个2

参考

  • Golang中的Defer必掌握的7知识点

感谢刘丹冰老师的总结,借此留笔记,如有版权纠纷,请速联系,立即删除。

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

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

相关文章

手写JavaScript中的Promise.all方法(JS中Promise.all的执行过程)

简介: Promise.all是JavaScript中一种用于处理多个Promise对象的方法,该方法接收一个数组作为参数,并返回一个新的Promise对象。 这个新的对象会在所有Promise对象都成功解析后解析,解析的结果是一个数组,包含了所有P…

Groovy(第一节)关于 Groovy

目录 什么是 Groovy? Groovy 快捷方式 Groovy 的新增特性 关于闭包 动态的 Groovy

论文阅读:SOLOv2: Dynamic, Faster and Stronger

目录 概要 Motivation 整体架构流程 技术细节 小结 论文地址:[2003.10152] SOLOv2: Dynamic and Fast Instance Segmentation (arxiv.org) 代码地址:GitHub - WXinlong/SOLO: SOLO and SOLOv2 for instance segmentation, ECCV 2020 & NeurIPS…

< JavaScript技巧:如何优雅的使用 【正则】校验 >

文章目录 👉 一、正则表达式的概念👉 二、常见使用正则表达式的方法① RegExp 对象方法1. 创建 RegExp 对象的语法2. RegExp对象方法① compile(value)② exec(value)③ test(value)③ reg.toString() ② 支持正则表达式的 String 对象的方法1. search()…

飞天使-学以致用-devops知识点1-安装gitlabharbor

文章目录 rpm 安装gitlab页面配置配置secretsecret 查看信息-chatgpt回复 为项目配置webhook,等jenkins部署完毕后在配置卸载 harbor配置secret所有k8s集群节点安装信任 http rpm 安装gitlab # 下载安装包 wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitla…

统计分析笔记3

文章目录 统计检验选择正确的统计检验统计检验是做什么的?何时进行统计检验选择参数化测试:回归、比较或相关性选择非参数检验 假设检验的假设条件skewness什么是零偏度right skewleft skew计算skewnesswhat to do if your data is skewed kurtosis怎么计…

BevFusion (2): nuScenes 数据介绍及点云可视化

1. nuScenes 数据集 1.1 概述 nuScenes 数据集 (pronounced /nu:ːsiː:nz/) 是由 Motional (以前称为 nuTonomy) 团队开发的自动驾驶公共大型数据集。nuScenes 数据集的灵感来自于开创性的 KITTI 数据集。 nuScenes 是第一个提供自动驾驶车辆整个传感器套件 (6 个摄像头、1 …

突破编程_C++_面试(抽象类与接口)

面试题 1 :简述抽象类的概念,并给出其使用场景的一个例子。 抽象类是面向对象编程中的一个重要概念,它定义了一组方法,但并不完全实现它们(可以包含成员变量、构造方法、析构方法以及普通方法的实现)。抽象…

计算机网络:IP

引言: IP协议是互联网协议族中的核心协议之一,负责为数据包在网络中传输提供路由寻址。它定义了数据包如何在互联网上从源地址传输到目的地址的规则和流程。IP协议使得各种不同类型的网络设备能够相互通信,实现了全球范围内的信息交换。 目录…

Qt项目:网络1

文章目录 项目:网路项目1:主机信息查询1.1 QHostInfo类和QNetworkInterface类1.2 主机信息查询项目实现 项目2:基于HTTP的网络应用程序2.1 项目中用到的函数详解2.2 主要源码 项目:网路 项目1:主机信息查询 使用QHostI…

基于vue的图书管理系统的设计与实现

高校师生在教学中承受的压力越大就对知识拥有了更多的需求,而满足这一需求的最佳场所无疑就是图书馆。当前虽然信息技术在各个方面都发挥出重要作用,但是在相当多的高校图书馆中依然由工作人员手动完成图书借阅、归还及逾期提醒等所有工作,在…

如何使用Logstash搜集日志传输到es集群并使用kibana检测

引言:上一期我们进行了对Elasticsearch和kibana的部署,今天我们来解决如何使用Logstash搜集日志传输到es集群并使用kibana检测 目录 Logstash部署 1.安装配置Logstash (1)安装 (2)测试文件 &#xff…

集群分发脚本xsync

集群分发脚本xsync 一、简介二、环境准备三、添加到机器的 hosts 文件四、ping 命令测试五、SSH 配置5.1.本地先生成公钥和私钥5.2.将公钥拷贝到其他机器 六、xsync 脚本编写6.1.安装 rsync6.2.新建 xsync.sh6.3.xsync.sh脚本6.4.赋予脚本执行权限6.5.测试 endl 一、简介 配置…

完全分布式运行模式

完全分布式运行模式 分析:之前已经配置完成 ​ 1)准备3台客户机(关闭防火墙、静态ip、主机名称) ​ 2)安装JDK ​ 3)配置环境变量 ​ 4)安装Hadoop ​ 5)配置环境变量 ​ 6&am…

163邮箱SMTP端口号及服务器地址详细设置?

163邮箱SMTP端口号是什么?163邮件SMTP设置教程? 除了基本的邮箱账号和密码外,还需要了解SMTP服务器地址和端口号,以及相应的设置。这些设置对于确保邮件能够顺利发送至关重要。下面,蜂邮EDM将详细介绍163邮箱SMTP端口…

Ubuntu常用状态命令

目录 一、温度 1,查看CPU温度 2,查看硬盘温度 二、CPU状态 1,显示CPU的详细信息,包括型号、频率、缓存等 2,显示CPU架构、CPU核心数、线程数、频率等信息 三、登录状态 1,查看成功登录的用户 2&am…

2024年腾讯云4核8G12M配置的轻量服务器同时支持多大访问量?

腾讯云4核8G服务器支持多少人在线访问?支持25人同时访问。实际上程序效率不同支持人数在线人数不同,公网带宽也是影响4核8G服务器并发数的一大因素,假设公网带宽太小,流量直接卡在入口,4核8G配置的CPU内存也会造成计算…

第12届生物发酵产品与技术装备展火热登场-通用环境控制技术

参展企业介绍 合肥通用环境控制技术有限责任公司隶属于中国机械工业集团有限公司(世界500强排名279),是中央直接管理的国有重要骨干上市央企(国机通用 600444),是国家级高新技术企业、国家火炬计划重点高新…

关于大语言模型LLM相关的数据集、预训练模型、提示词、微调的文心一言问答

文章目录 关于大语言模型LLM相关的数据集、预训练模型、提示词、微调的文心一言问答先总结一下Q:LLM模型预训练前与提示词关系,LLM模型预训练后与提示词关系Q:预训练用的数据集与提示词有什么异同Q:为什么我看到的数据集结构和提示…

区块链智能合约开发

一.区块链的回顾 1.区块链 区块链实质上是一个去中心化、分布式的可进行交易的数据库或账本 特征: 去中心化:简单来说,在网络上一个或多个服务器瘫痪的情况下,应用或服务仍然能够持续地运行,这就是去中心化。服务和应用部署在…