当前位置: 首页 > article >正文

【新人系列】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 最终将会从堆上获取具体的值。

相关文章:

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

✍ 个人博客:https://blog.csdn.net/Newin2020?typeblog 📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html 📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们…...

知识图谱之知识抽取:从数据海洋中 “捞金”

目录 知识抽取:开启知识宝库的钥匙 知识抽取的对象:实体、关系与属性 知识抽取的方法:各显神通的 “淘金术” 基于规则的方法 机器学习方法 深度学习方法 知识抽取面临的挑战:荆棘丛中的探索 数据的多样性和复杂性 语义理…...

RAG - 五大文档切分策略深度解析

文章目录 切分策略1. 固定大小分割(Fixed-Size Chunking)2. 滑动窗口分割(Sliding Window Chunking)3. 自然语言单元分割(Sentence/Paragraph Segmentation)4. 语义感知分割(Semantic-Aware Seg…...

keil中文注释出现乱码怎么解决

keil中文注释出现乱码怎么解决 在keil–edit–configuration中encoding改为chinese-GB2312...

论文阅读笔记——ReconDreamer

ReconDreamer 论文 在 DriveDreamer4D 的基础上,通过渐进式数据更新,解决大范围机动(多车道连续变道、紧急避障)的问题。同时 DriveDreamer4D生成轨迹后直接渲染,而 ReconDreamer 会实时通过 DriveRestorer 检测渲染结…...

鸿蒙harmonyOS:笔记 正则表达式

从给出的文本中,按照既定的相关规则,匹配出符合的数据,其中的规则就是正则表达式,使用正则表达式,可以使得我们用简洁的代码就能实现一定复杂的逻辑,比如判断一个邮箱账号是否符合正常的邮箱账号&#xff0…...

计算机网络——传输层(TCP)

传输层 在计算机网络中,传输层是将数据向上向下传输的一个重要的层面,其中传输层中有两个协议,TCP,UDP 这两个协议。 TCP 话不多说,我们直接来看协议报头。 源/目的端口号:表示数据从哪个进程来&#xff0…...

英伟达与通用汽车深化合作,澳特证券am broker助力科技投资

在近期的GTC大会上,英伟达CEO黄仁勋宣布英伟达将与通用汽车深化合作,共同推进AI技术在自动驾驶和智能工厂的应用。此次合作标志着自动驾驶汽车时代的加速到来,同时也展示了英伟达在AI技术领域的最新进展。      合作内容包括:…...

Visual Studio 2022静态库与动态库创建及使用完全指南

在C开发中,库(Library)是代码复用的重要方式。本教程将详细介绍如何在Visual Studio 2022中创建和使用静态库(.lib)和动态库(.dll),每种库类型都会有完整的创建步骤和实际示例。 第…...

C++中常见符合RAII思想的设计有哪些

文章目录 **一、标准库中的 RAII 类**1. **智能指针**2. **文件操作类**3. **锁管理类**4. **容器类**5. **线程管理** **二、自定义 RAII 类的常见场景**1. **数据库连接**2. **图形资源管理(如 OpenGL 纹理)**3. **网络套接字**4. **事务处理**5. **临…...

CUDA Memory Fence 函数的功能与硬件实现细节

CUDA Memory Fence 函数的功能与硬件实现细节 Memory Fence 的基本功能 CUDA中的memory fence函数用于控制内存操作的可见性顺序,确保在fence之前的内存操作对特定范围内的线程可见。主要功能包括: 排序内存操作:确保fence之前的内存操作在…...

CSS学习笔记5——渐变属性+盒子模型阶段案例

目录 通俗易懂的解释 渐变的类型 1、线性渐变 渐变过程 2、径向渐变 如何理解CSS的径向渐变,以及其渐变属性 通俗易懂的解释 渐变属性 1. 形状(Shape) 2. 大小(Size) 3. 颜色停靠点(Color Sto…...

[Java微服务架构]4_服务通信之客户端负载均衡

欢迎来到啾啾的博客🐱,一个致力于构建完善的Java程序员知识体系的博客📚,记录学习的点滴,分享工作的思考、实用的技巧,偶尔分享一些杂谈💬。 欢迎评论交流,感谢您的阅读&#x1f604…...

基于SpringBoot实现的高校实验室管理平台功能四

一、前言介绍: 1.1 项目摘要 随着信息技术的飞速发展,高校实验室的管理逐渐趋向于信息化、智能化。传统的实验室管理方式存在效率低下、资源浪费等问题,因此,利用现代技术手段对实验室进行高效管理显得尤为重要。 高校实验室作为…...

吴恩达深度学习复盘(1)神经网络与深度学习的发展

一、神经网络的起源与生物学动机 灵感来源 神经网络的最初动机源于对生物大脑的模仿。20 世纪 50 年代,科学家试图通过软件模拟神经元的工作机制(如树突接收信号、轴突传递信号),构建类似人类大脑的信息处理系统。 生物神经元的简…...

用Python实现资本资产定价模型(CAPM)

使用 Python 计算资本资产定价模型(CAPM)并获取贝塔系数(β)。 步骤 1:导入必要的库 import pandas as pd import yfinance as yf import statsmodels.api as sm import matplotlib.pyplot as plt 步骤 2&#xff1…...

Linux进程管理之子进程的创建(fork函数)、子进程与线程的区别、fork函数的简单使用例子、子进程的典型应用场景、父进程等待子进程结束后自己再结束

收尾 进程终止:子进程通过exit()或_exit()终止,父进程通过wait()或waitpid()等待子进程终止,并获取其退出状态。?其实可以考虑在另一篇博文中来写 fork函数讲解 fork函数概述 fork() 是 Linux 中用于创建新进程的系统调用。当…...

妙用《甄嬛传》中的选妃来记忆概率论中的乘法公式

强烈推荐最近在看的不错的B站概率论课程 《概率统计》正课,零废话,超精讲!【孔祥仁】 《概率统计》正课,零废话,超精讲!【孔祥仁】_哔哩哔哩_bilibili 其中概率论中的乘法公式,老师用了《甄嬛传…...

虚幻基础:UI

文章目录 控件蓝图可以装载其他控件蓝图可以安装其他蓝图接口 填充:相对于父组件填充水平框尺寸—填充—0.5:改变填充的尺寸填充—0.5:改变与父组件的距离 锚点:相对于父组件的控件坐标系原点,屏幕比例改变时&#xff…...

【MySQL篇】事务管理,事务的特性及深入理解隔离级别

目录 一,什么是事务 二,事务的版本支持 三,事务的提交方式 四,事务常见操作方式 五,隔离级别 1,理解隔离性 2,查看与设置隔离级别 3,读未提交(read uncommitted&a…...

项目实战-角色列表

抄上一次写过的代码: import React, { useState, useEffect } from "react"; import axios from axios; import { Button, Table, Modal } from antd; import { BarsOutlined, DeleteOutlined, ExclamationCircleOutlined } from ant-design/icons;const…...

fetch`的语法规则及常见用法

fetch() 是 JavaScript 用于发送 HTTP 请求的内置 API,功能强大,语法简洁。以下是 fetch 的语法规则及常见用法。 1. fetch 基本语法 fetch(url, options).then(response > response.json()) // 解析 JSON 响应体.then(data > console.log(data))…...

如何排查java程序的宕机和oom?如何解决宕机和oom?

排查oom 用jmap生成我们的堆空间的快照Heap Dump(堆转储文件),来分析我们的内存占用 用可视化工具,例如java中的jhat分析Heap Dump文件 ,它分析完会通过一个浏览器打开一个可视化页面展示分析结果 根据oom的类型来调…...

26_ajax

目录 了解 接口 前后端交互 一、安装服务器环境 nodejs ajax发起请求 渲染响应结果 get方式传递参数 post方式传递参数 封装ajax_上 封装ajax下 了解 清楚前后端交互就可以写一些后端代码了。小项目 现在写项目开发的时候都是前后端分离 之前都没有前端这个东西&a…...

代理模式(Proxy Pattern)实现与对比

代理模式(Proxy Pattern)实现与对比 1. 虚拟代理(Virtual Proxy) 定义:延迟加载对象,避免资源浪费。 适用场景:大文件或资源的加载(如图片、数据库连接)。 代码示例 /…...

MySQL - 数据库基础操作

SQL语句 结构化查询语言(Structured Query Language),在关系型数据库上执行数据操作、数据检索以及数据维护的标准语言。 分类 DDL 数据定义语言(Data Definition Language),定义对数据库对象(库、表、列、索引)的操作。 DML 数据操作语言(Data Manip…...

​​​​​​Spring Boot热部署插件

在实际开发中,我们修改某些代码或页面都需要重启应用后才能生效,如果每次都手动重启,会降低了开发效率;热部署是指当我们修改代码后,服务能自动重启加载新修改的内容,这样大大提高了我们开发的效率&#xf…...

pip install cryptacular卡住,卡在downloading阶段

笔者安装pip install cryptacular卡在downloading阶段,但不知道为何 Collecting cryptacularCreated temporary directory: /tmp/pip-unpack-qfbl8f08http://10.170.22.41:8082 "GET http://repo.huaweicloud.com/repository/pypi/packages/42/69/34d478310d6…...

AI大模型从0到1记录学习 day09

第 8 章 面向对象之类和对象 8.1 面向过程和面向对象 面向过程编程(Procedural Programming)和面向对象编程(OOP)是两种不同的编程范式,它们在软件开发中都有广泛的应用。 Python是一种混合型的语言,既支持…...

【FW】ADB指令分类速查清单

1. 设备管理 指令核心作用adb devices列出已连接设备adb reboot重启设备adb reboot bootloader进入Bootloader模式adb reboot recovery进入Recovery模式adb root获取Root权限(需设备支持)adb remount挂载系统分区为可读写 2. 应用管理 指令核心作用adb…...