【新人系列】Golang 入门(八):defer 详解 - 上

✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog
📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~
📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Golang 的学习。在这个 Golang 的新人系列专栏下,将会总结 Golang 入门基础的一些知识点,并由浅入深的学习这些知识点,方便大家快速入门学习~
❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪

1. 快速了解

defer 后面的代码会在函数 return 后执行,并且执行的顺序是与代码的顺序相反,即倒序执行。

//main 2 1
func main() {defer fmt.Println("1")defer fmt.Println("2")fmt.Println("main")return
}

使用 defer 需要注意其执行的时机,以免造成意料之外的影响,例如它可能会修改返回值:

func deferReturn() (ret int) {defer func() {ret++}()return 10
}func main() {ret := deferReturn()fmt.Printf("ret = %d\r\n",ret)    //11
}

2. defer 执行逻辑

我们先来看一段简洁的代码。

func A() {defer B()//code to do something
}

上面这段代码,编译后的伪指令是下面这样的。defer 指令对应到两部分内容,其中 deferproc 负责把要执行的函数信息保存起来,我们称之为 defer 注册。而 deferproc 函数会返回 0,下面 if 分支和 panic recover 有关,可以先忽略不看,同时对应要跳转的 ret 这里也先忽略不看。

func A() {r = deferproc(8, B)if r > 0 {goto ret}//code to do somethingruntime.deferreturn()return
ret:runtime.deferreturn()
}

去掉忽略的部分,程序的整体逻辑就比较清晰了。在 defer 注册完成后,程序就会执行后面的逻辑,直到返回之前通过 deferreturn 执行注册的 defer 函数,即 defer 调用。正是因为先注册后调用,才实现了 defer 延迟执行的效果。

func A() {r = deferproc(8, B)    // 1.注册//code to do somethingruntime.deferreturn()  // 2.调用return
}

看回 defer 注册部分,defer 注册的信息会注册到一个链表,而当前执行的 goroutine 会持有这个链表的头指针。每个 goroutine 在运行时都有一个对应的结构体 g,其中有一个字段就指向 defer 链表头。

defer 链表链起来的是一个一个 _defer 结构体,新注册的 defer 会添加到链表头,执行时也是从头开始,这也就是 defer 会表现为倒序执行的原因。

在这里插入图片描述

在展开 _defer 结构之前,先看一个例子,这里函数 A 注册了一个 defer 函数 A1。

func A1(a int) {fmt.Println(a)
}
func A() {a, b := 1, 2defer A1(a)a = a + bfmt.Println(a, b)
}

我们来看看函数调用栈,A 的栈帧首先会是存放两个局部变量。接着 A1 只有一个参数,因此局部变量下面存放参数 a 的值 1,然后就要注册 defer 函数 A1 了。

在这里插入图片描述

deferproc 函数原型只有两个参数,第一个参数是 defer 函数 A1 的参数加返回值共占多大空间。这里 A1 没有返回值,只需要一个整形参数和一个指针变量,因此 64 位下要占 4 字节。

func deferproc (siz int32, fn *funcval)

第二个参数是一个 function value,前面函数部分我们也介绍过,没有捕获列表的 function value 在编译阶段就会做出优化,即在只读数据段分配一个共用的 funcval 结构体,结构体中的指针会指向函数 A1 指令入口,所以 deferproc 的第二个参数就是结构体的地址 addr2。

func deferproc (siz = 4, fn = addr2)

在这里插入图片描述

至此我们先把 _defer 的结构体展开了看一下:

type _defer struct {siz     int32     // 参数和返回值共占多少字节,这段空间会直接分配在_defer结构体后面,用于在注册时保存参数,并在执行时拷贝到调用者参数与返回值空间started bool      // 标记defer是否已经执行sp      uintptr   // 记录注册这个defer的函数栈指针(调用者栈指针),函数可以通过它判断自己注册的defer是否已经执行完了pc      uintptr   // deferproc的返回地址fn      *funcval  // 注册的function value函数_panic  *_paniclink    *_defer   // 链接到前一个注册的defer结构体
}

当 deferproc 函数调用时,编译器会在后面继续开辟一段空间,用于存放 defer 函数的返回值和参数,由于在这个例子里没有返回值,因此只分配 defer 函数的一个参数的空间,这一段空间会被直接拷贝到 _defer 结构体的后面。

另外,返回值地址和调用者函数的 BP 则放在 deferproc 两个参数之后。

在这里插入图片描述

在 deferproc 函数执行时,需要堆分配一段空间用于存放 _defer 结构体,而在 _defer 结构体后面也会分配一段空间用于存放 siz 大小的参数与返回值,这里由于没有返回值因此存放参数 a。(注意这里所有的变量存放的顺序是从下至上的,因此参数 a 虽然说是存放在 _defer 结构体的后面,但其实分配的空间在该结构体存放的位置之上)

在这里插入图片描述

然后这个 _defer 结构体就会被添加到 defer 链表头,至此 deferproc 注册结束。

_defer 结构体预分配
实际上 go 语言会预分配不同规格的 defer 池,执行时从空闲的 _defer 中取一个出来用即可。如果没有空闲的或者没有大小合适的,则会再进行堆分配,用完以后再放回空闲的 _defer 池,这样就可以避免频繁地堆分配与回收。

让我们再回到函数代码的执行,当代码执行到函数 A 中的 a = a + b 这行代码时,变量 a 被赋值为 3,然后下一步会输出局部变量 a 和 b 的值,即 3 和 2。

在这里插入图片描述

接下来就到 deferreturn 执行 defer 链表了,此时会从当前 goroutine 拿到链表头上的这个 _defer 结构体,通过 _defer 结构体里的 fn = addr2 找到对应的 funcval,然后通过 funcval 中的 fn 可以拿到函数入口的地址 addr1。

在调用 A1 时,会把 _defer 后面的参数与返回值整个拷贝到 A1 的调用者栈上,然后 A1 开始执行,此时就会输出 1。

这里的关键是 defer 函数的参数在注册时拷贝到堆上,执行时又拷贝到栈上。并不会去使用到 A 函数栈中保存的局部变量 a 的值 3,所以即使在 defer 函数注册后修改了这个局部变量 a 的值,也不会影响到执行 defer 函数时用到的变量 a。

在这里插入图片描述

既然 deferproc 注册的是一个 function value,我们下面就来看看捕获列表时是什么情况,变量 a 在 defer 函数注册后进行修改是否能影响到 defer 函数里使用的变量。

3. defer + 闭包

在下面这个例子中,defer 函数不止要传递局部变量 b 做参数,还捕获了外层函数的局部变量 a 并形成了闭包。

func A() {a, b := 1, 2defer func(b int) {a = a + bfmt.Println(a, b)}(b)a = a + bfmt.Println(a, b)
}

匿名函数会由编译器按照 A_func1 这样的形式命名。如下图所示,假设这个闭包函数的指令入口地址为 addr1。

由于捕获变量 a 除了初始化赋值外还被修改过,所以局部变量 a 改为堆分配,而栈上存储它的地址。另外,还有一个局部变量 b 也要分配。

在这里插入图片描述

然后创建闭包对象,堆分配一个 funcval 结构体,并且捕获列表中存储 a 的地址。

deferproc 执行时,_defer 结构体中的 fn 就是这个 funcval 结构体的起始地址。除此之外,还要拷贝参数 b 的值到 _defer 结构体的后面,然后把这个 _defer 结构体添加到 defer 链表头。

在这里插入图片描述

至此,deferproc 注册结束。然后接着执行到 a = a + b 这行代码,变量 a 被赋值为 3。而下一步就自然输出 a 和 b 的变量值,即 3 和 2。

在这里插入图片描述

接着就到 deferreturn 了,从 defer 链表头拿到这个 defer 结构体,执行注册的 defer 函数时,需要把参数 b 拷贝到栈上的参数空间。

另外,闭包函数也会通过寄存器存储的 funcval 地址加上偏移,找到捕获变量 a 的地址。

在这里插入图片描述

当执行到 defer 函数 A_func1 里的 a = a + b 这行代码时,此时的 a = 3 且 b = 2,所以 a 会被赋值为 5。因此,下一步将会输出变量 a 和 b 的值,即 5 和 2。

在这里插入图片描述

可以发现当变量 a 变成被捕获的变量形成闭包后,在注册完 defer 函数后修改变量 a 是可以影响到 defer 函数中使用的变量值的。这是因为此时的变量 a 发生了逃逸,不再分配到栈上而是分配到堆上,defer 函数的变量 a 最终将会从堆上获取具体的值。

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

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

相关文章

鸿蒙开发:了解Canvas绘制

前言 本文基于Api13 系统的组件无法满足我们的需求,这种情况下就不得不自己自定义组件,除了自定义组合组件,拓展组件,还有一种方式,那就是完全的自绘制组件,这种情况,常见的场景有,比…

【Linux笔记】进程间通信——命名管道

🔥个人主页🔥:孤寂大仙V 🌈收录专栏🌈:Linux 🌹往期回顾🌹:【Linux笔记】进程间通信——匿名管道||进程池 🔖流水不争,争的是滔滔不 一、命名管道…

Spring项目中使用EasyExcel实现Excel 多 Sheet 导入导出功能(完整版)

Excel 多 Sheet 导入导出功能完整实现指南 一、环境依赖 1. Maven 依赖 <!-- EasyExcel --> <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version> </dependency>…

全流程剖析需求开发:打造极致贴合用户的产品

全流程剖析需求开发&#xff1a;打造极致贴合用户的产品 一、需求获取&#xff08;一&#xff09;与用户沟通1.面谈2.问卷调查3.会议讨论 &#xff08;二&#xff09;观察用户工作&#xff08;三&#xff09;收集现有文档 二、需求分析&#xff08;一&#xff09;提炼关键需求&…

SQL语句及其应用(中)(DQL语句之单表查询)

SQL语句的定义: 概述: 全称叫 Structured Query Language, 结构化查询语言, 主要是实现 用户(程序员) 和 数据库软件(例如: MySQL, Oracle)之间交互用的. 分类: DDL: 数据定义语言, 主要是操作 数据库, 数据表, 字段, 进行: 增删改查(CURD) 涉及到的关键字: create, drop, …

5000元组装一台本地运行中、小模型主机,参考配置 (运行DeepSeek、Qwen)

5000元组装一台本地运行中、小模型主机&#xff0c;参考配置 &#xff08;运行DeepSeek、Qwen) 5000元中、小模型主机 DeepSeek、Qwen 各精度模型推荐启动方式 模型名称 参数量 精度 模型大小 推荐运行模式 DeepSeek R1 7b Q4 5 GB LM Studio纯GPU 14b Q4 9 GB LM…

【新手初学】SQL注入getshell

一、引入 木马介绍&#xff1a; 木马其实就是一段程序&#xff0c;这个程序运行到目标主机上时&#xff0c;主要可以对目标进行远程控制、盗取信息等功能&#xff0c;一般不会破坏目标主机&#xff0c;当然&#xff0c;这也看黑客是否想要搞破坏。 木马类型&#xff1a; 按照功…

Containerd+Kubernetes搭建k8s集群

虚拟机环境设置&#xff0c;如果不是虚拟机可以忽略不看 1、安装配置containerd 1.1 添加 Kubernetes 官方仓库 安装cri-tools的时候需要用到 cat > /etc/yum.repos.d/kubernetes.repo << EOF [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kub…

应用待机分组管控是啥

1. 应用待机群组是啥&#xff1f; Android 9 引入了一个新功能&#xff0c;叫应用待机群组。简单来说&#xff0c;就是根据你最近使用应用的频率和时间&#xff0c;系统会把应用分成不同的“群组”。每个群组的应用能用的系统资源不一样&#xff0c;比如后台任务、闹钟、网络请…

C/C++后端开发面经

字节跳动 客户端开发 实习 一面(50min) 自我介绍是否愿意转语言,是否只愿意搞后端选一个项目来详细谈谈HTTP和HTTPS有什么区别?谈一下HTTPS加密的具体过程&#xff1a; 非对称加密 对称加密 证书认证的方式 非对称加密是为了保证对称密钥的安全性。 对称…

【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】A题解题全流程(持续更新)

【第十三届“泰迪杯”数据挖掘挑战赛】【2025泰迪杯】A题解题全流程-思路&#xff08;持续更新&#xff09; 写在前面&#xff1a; 1、A题、C题将会持续更新&#xff0c;陆续更新发布文章 2、赛题交流咨询Q群&#xff1a;1037590285 3、全家桶依旧包含&#xff1a; 代码、…

如何让 history 记录命令执行时间?Linux/macOS 终端时间戳设置指南

引言:你真的会用 history 吗? 有没有遇到过这样的情况:你想回顾某个重要命令的执行记录,却发现 history 只列出了命令序号和内容,根本没有时间戳?这在运维排查、故障分析、甚至审计时都会带来极大的不便。 想象一下,你在服务器上误删了某个文件,但不知道具体是几点执…

Redis缓存异常场景深度解析:穿透、击穿、雪崩及终极解决方案

一、引言 在高并发系统中&#xff0c;缓存承担着流量洪峰的削峰填谷作用。然而当缓存层出现异常时&#xff0c;可能引发数据库级联崩溃&#xff0c;造成系统瘫痪。本文将深入剖析缓存穿透、缓存击穿、缓存雪崩三大典型问题&#xff0c;并提供企业级解决方案。文章包含7种防御策…

Scala 之 正则

regex 函数提取 import scala.util.matching.Regex// 输入表达式 val expression "[a#0, round(a#0, 0) AS round(a, 0)#1, abs(a#0) AS abs(a)#2, len(cast(a#0 as string)) AS len(a)#3]"// 定义一个正则表达式来提取函数名称 val functionPattern: Regex &quo…

CI/CD-Jenkins安装与应用

CI/CD-Jenkins安装与应用 Docker安装Jenkins docker-compose.yaml version: "3.8" # # 自定义网络配置 # networks:cicd:driver: bridgeservices:jenkins:# 尽量使用新版本的Jenkins, 低版本的Jenkins的有些插件使用不了# jenkins/jenkins:lts-jdk17是长期支持版…

验证Linux多进程时间片切换的程序

​​ 一、软件需求 在同时运行多个CPU密集型进程时&#xff0c;需采集以下统计信息&#xff1a; 当前运行在逻辑CPU上的进程ID每个进程的运行进度百分比 实验程序设计要求&#xff1a; 1. 命令行参数 参数说明示例值n并发进程数量3total总运行时长&#xff08;毫秒&…

IvorySQL:兼容Oracle数据库的开源PostgreSQL

今天给大家介绍一款基于 PostgreSQL 开发、兼容 Oracle 数据库的国产开源关系型数据库管理系统&#xff1a;IvorySQL。 IvorySQL 由商瀚高软件提供支持&#xff0c;主要的功能特性包括&#xff1a; 完全兼容 PostgreSQL&#xff1a;IvorySQL 基于 PostgreSQL 内核开发&#xf…

树莓派超全系列文档--(13)如何使用raspi-config工具其二

如何使用raspi-config工具其二 raspi-configPerformance optionsOverclockGPU memoryOverlay file systemFan Localisation optionsLocaleTime zoneKeyboardWLAN country Advanced optionsExpand filesystemNetwork interface namesNetwork proxy settingsBoot orderBootloader…

QT音乐播放器(1):数据库保存歌曲

实现功能&#xff1a;用数据库保存本地导入和在线搜索的歌曲记录 目录 一. 保存本地添加的歌曲 1. 使用QSettings &#xff08;1&#xff09;在构造函数中&#xff0c;创建对象。 &#xff08;2&#xff09;在导入音乐槽函数中&#xff0c;保存新添加的文件路径&#xff0c…

自动化发布工具CI/CD实践Jenkins常用工具和插件的使用

1、安装常用工具 名称版本备注jdkjava8代码打包所需git1.8.3.1maven3.6.3注意配置私服内容nvm0.39.3多Node.js环境管理工具Node.jsv14.18.0 / v16.17.1包管理工具yarn1.22.15包管理工具 1.1 安装jdk Jenkins 需要使用java11 及以上&#xff0c;但是代码打包依赖jdk8&#xff…