Golang架构直通车——理解defer

2023-11-12

defer应用

Go 语言的 defer会在当前函数或者方法返回之前执行传入的函数。它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。
比如解锁资源:

mu.Lock()
defer mu.Unlock()

我们在 Go 语言中使用 defer 时会遇到两个比较常见的问题,这里会介绍具体的场景并分析这两个现象背后的设计原理:

  1. defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的;
  2. defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果;

defer触发时机

defer的触发时机主要有三个:

  1. 函数执行到函数体末端
  2. 函数执行return语句
  3. 当前协程panic

defer执行顺序

直接用go程序演示:

func main() {
	for i := 0; i < 5; i++ {
		defer fmt.Println(i)
	}
}

其输出为:

4 3 2 1 0

运行上述代码会倒序执行所有向 defer 关键字中传入的表达式,最后一次 defer 调用传入了 fmt.Println(4),所以会这段代码会优先打印 4。

我们可以通过下面这个简单例子强化对 defer 执行时机的理解:

func main() {
    {
        defer fmt.Println("defer runs")
        fmt.Println("block ends")
    }
    
    fmt.Println("main ends")
}
$ go run main.go
block ends
main ends
defer runs

defer 传入的函数不是在退出代码块的作用域时执行的,它会在当前函数和方法返回之前被调用。

预计算参数

假设我们想要计算 main 函数运行的时间,可能会写出以下的代码:

func main() {
	startedAt := time.Now()
	defer fmt.Println(time.Since(startedAt))
	
	time.Sleep(time.Second)
}
$ go run main.go
0s

我们理想的输出结果应该是1s,但是上述代码的运行结果并不符合我们的预期。

调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 0s。

想要解决这个问题的方法非常简单,我们只需要向 defer 关键字传入匿名函数:

func main() {
	startedAt := time.Now()
	defer func() { fmt.Println(time.Since(startedAt)) }()
	
	time.Sleep(time.Second)
}
$ go run main.go
1s

虽然调用 defer 关键字时也使用值传递,但是因为拷贝的是函数指针,所以 time.Since(startedAt)会在 main 函数返回前被调用并打印出符合预期的结果。

defer实现原理

首先来了解一下 defer 关键字在 Go 语言源代码中对应的数据结构,defer数据结构的源码在src/runtime/runtime2.go中定义:

type _defer struct {
	siz     int32
	started bool
	sp      uintptr
	pc      uintptr
	fn      *funcval
	_panic  *_panic
	link    *_defer
}

我们简单介绍一下 runtime._defer 结构体中的几个字段:

  1. siz 是参数和结果的内存大小;
  2. sppc 分别代表栈指针和调用方的程序计数器;
  3. fn 是 defer 关键字中传入的函数;
  4. _panic 是触发延迟调用的结构体,可能为空;
  5. link:注意到:*_defer,说明了这个数据结构实际上是一个链表。
    在这里插入图片描述

除了上述的这些字段之外,runtime._defer 中还包含一些垃圾回收机制使用的字段,这里为了减少理解的成本就都省去了。

中间代码生成阶段执行的被 cmd/compile/internal/gc.state.stmt 函数会处理 defer 关键字。从下面截取的这段代码中,我们会发现编译器调用了 cmd/compile/internal/gc.state.call 函数,这表示 defer 在编译器看来也是函数调用:

func (s *state) stmt(n *Node) {
	switch n.Op {
	case ODEFER:
		s.call(n.Left, callDefer)
	}
}

对于defer关键字,主要有3个函数:

  1. deferproc。在每遇到一个defer关键字时,实际上都会转换为deferproc函数,deferproc函数的作用是将defer函数存入链表中
  2. deferreturn。在return指令前调用,从链表中取出defer函数并执行
  3. deferprocStack。go1.13后对defer做的优化,通过利用栈空间提高效率。
  • 编译期;
    • 将 defer 关键字被转换 runtime.deferproc
    • 在调用 defer 关键字的函数返回之前插入 runtime.deferreturn
  • 运行时:
    • runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
    • runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;

如果要了解详情,参考:理解Go语言defer关键字的原理

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Golang架构直通车——理解defer 的相关文章

  • 基本类型、包装类型与自动拆装箱

    Java的8种数据类型 Java 的每个基本类型都对应了一个包装类型 比如说 int 的包装类型为 Integer double 的包装类型为 Double 基本类型 包装类 boolean Boolean byte Byte short
  • 最适合练手的第一个Qt小程序,所有代码均可复制

    文章目录 前言 一 最适合新手的第一个Qt小程序 1 1 按钮的创建 1 2 对象模型 对象树 二 Qt窗口坐标体系 三 信号和槽机制 3 1 系统自带的信号和槽 3 2 自定义信号和槽 3 3 信号槽的扩展 3 4 Qt4版本的信号和槽写
  • JWT Token 的构成以及生成过程

    一 jwt token 是什么样子的 JWT是由三段信息构成的 将这三段信息文本用点链接一起就构成了Jwt字符串 JWT字符串 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 eyJzdWIiOiIxMjM0NTY
  • 06-1_Qt 5.9 C++开发指南_对话框与多窗体设计_标准对话框

    在一个完整的应用程序设计中 不可避免地会涉及多个窗体 对话框的设计和调用 如何设计和调用这些对话框和窗体是搞清楚一个庞大的应用程序设计的基础 本章将介绍对话框和多窗体设计 调用方式 数据传递等问题 主要包括以下几点 Qt 提供的标准对话框的
  • 给定一个非负整数数组,你最初位于数组的第一个位置... --错误方法纠正

    力扣55题 闹心 太闹心了 上周空闲时间写了一个自动钻取的 结果发现把0搞进去之后各种问题 房子越补漏雨越大 刚才琢磨了一下 真的是方向错了 先来聊聊之前的方案 以此数组为例 int nums 2 1 1 1 3 lastIndex num
  • 最新deepin-wine下微信的安装方法,非常简单 Ubuntu linux可用

    deepin wine阿里云镜像访问异常 可以使用以下脚本安装最新版deepin wine 微信最新版本 deepin com wechat 2 6 8 65deepin0 i386 deb 下载网址 Index of deepin poo
  • 数据结构与算法目录

    前言 数据结构与算法系列先看这里 有助于你更好地获取内容 首先明白一个问题 为什么要研究数据结构 这是因为所有的程序本质上是对数据进行处理 如何高效的处理数据 这依赖于数据本身的结构 如类型 整型 浮点型等 维数 是否为复杂类型 结构体类型
  • 常用Python PDF库对比

    2022 06 07修订 新增第三方库borb 初稿写于2021 01 02 彼时borb才发布1 0版没几个月 两年不到 Github上已近三千赞 PDF Portable Document Format 是一种便携文档格式 便于跨操作系
  • 故障树

    故障树 时间20210105 可以根据这道题目学习故障树 假设系统的可靠性逻辑框图如下所示 故障树的定义 用以表明产品哪些组成部分的故障或外界事件或它们的组合将导致产品发生一种给定故障的逻辑图 故障树是一种逻辑因果关系图 构图的元素是事件和
  • Keil编译错误error: #20: identifier "XXXX" is undefined

    问题 在使用Keil编译工程时 经常遇到提示identifier XXXX 未定义的error信息 Build target canopen mx v1d61 compiling main c Src main c 104 warning
  • SMT贴片制造:发挥的作用和价值]

    SMT贴片制造作为一项重要的电子制造技术 发挥着举足轻重的作用 并提供了巨大的价值 首先 SMT贴片制造为电子产品的制造商提供了高效 准确和可靠的生产方式 相比于传统的手工焊接 SMT贴片制造具有更高的自动化和智能化程度 大幅提高了生产效率
  • day21网络编程(下)

    day21 网络编程 下 课程目标 学会网络编程开发的必备知识点 今日概要 OSI7 层模型 TCP和UDP 粘包 阻塞和非阻塞 IO多路复用 1 OSI 7层模型 OSI的7层模型对于大家来说可能不太好理解 所以我们通过一个案例来讲解 假
  • 安装【sonar】【sonarQube】免费社区版9.9

    文章目录 sonarQube 镜像容器 Linux 安装镜像 出现 Permission denied的异常 安装sonarQube 中文包 重启服务 代码上传到sonarQube扫描 java语言配置 配置 JS TS Php Go Py
  • gitee项目克隆到本地并运行

    首先电脑上要先安装node js和git 配置一下 在gitee上找到需要克隆到本地的项目 点击克隆 下载 在点击HTTPS下面的复制 github上也是类似的方式 把项目地址复制下来 在本机电脑 新建一个文件夹 命名任意 打开新建的文件夹
  • 神经元模型介绍

    一 深度学习的背景 目前 深度学习 Deep Learning 简称DL 在算法领域可谓是大红大紫 深度学习是机器学习领域中的一个新的研究方向 模仿生人类神经网络 学习样本数据的内在规律的一种方法 神经网络属于监督学习的过程 可以处理 回归
  • SQL 语句学习总结:

    1 四范式 范式好处 数据库范式是数据表设计的规范 在范式规范下 数据库里每个表存储的重复数据降到最少 这有助于数据的一致性维护 同时在数据库范式下 表和表之间不再有很强的数据耦合 可以独立的增长 ie 比如汽车引擎的增长和汽车的增长是完全
  • 接口响应时间长,前端返回请求超时解决

    在前端代码设置axios响应时间 1 全局设置 axios defaults timeout 时间 单位为毫秒 或 2 封装的http请求 const service axios create 公共接口 这里注意后面会讲 baseURL p

随机推荐

  • 解决Flutter输入框限制最大输入长度时,从中间输入会自动截取掉后面的内容

    一 问题原由 当限制输入框最大输入长度时 将光标移动到中间输入 当输入的内容长度加上已经输入内容的长度大于限制的输入长度时 Flutter会将光标后面的内容进行截取掉 而当我在原生Android上验证时却是自动截取输入的内容原有的内容不动
  • 输出数组中最大、小值和下标

    详细看代码 package exp 4 public class Array01 public static void main String args int arrs 1 2 2 12 7 5 声明数组并赋值 int max arrs
  • Python实现目录文件扫描功能

    日常程序编写中常常遇到需要获取目录下文件的功能 对该功能做个简单整理 供大家参考 实现遍历目录文件最常用的方法是os listdir 还有一种os walk方法 一 os listdir方法 源码中对该方法的描述 Return a list
  • 史上最详细黑盒测试用例方法总结(等价类、边界值、因果图等)

    黑盒测试用例设计方法 一 等价类 等价类划分法原理 1 把程序的输入域划分成若干部分 然后从每个部分中选取少数代表性数据作为测试用例 2 每一类的代表性数据在测试中的作用等价于这一类中的其他值 如果某一类中的一个例子发现了错误 这一等价类中
  • Flutter FutureBuilder

    FutureBuilder 是 Flutter 中的一个小部件 用于根据 Future 的结果构建用户界面 它接受一个 Future 对象和一个构建函数作为参数 FutureBuilder 将监听 Future 对象的变化 并相应地更新用户
  • useCallback 作用,useMemo ,memo作用 浅显理解

    项目中看到别人代码基本上每个函数都写了useCallback 于是去查了查到底有什么作用 快看睡着了还是没太明白 直接同事请教了一下 大概浅显的理解一点 useCallback 简单来说就是返回一个函数 只有在依赖项发生变化的时候才会更新
  • Vue 点击导航栏滑动到指定位置

    效果图 assignBlock gif 方法1
  • 前后端分离接口

    前后端分离接口的意义 目前现有前后端开发模式 后端为主的MVC时代 如下图所示 代码可维护性得到明显好转 MVC 是个非常好的协作模式 从架构层面让开发者懂得什么代码应该写在什么地方 为了让 View 层更简单干脆 还可以选择 Veloci
  • 现代密码学期末总结

    文章目录 写在前面 1 引言 知识点 习题 2 流密码 知识点 习题 3 分组密码 知识点 习题 4 公钥密码 知识点 习题 5 数字签名 知识点 习题 6 哈希函数 知识点 7 认证技术 知识点 8 密钥分配与密钥管理 知识点 习题 9
  • 大数据:对大数据的理解

    学了这么久大数据 学了好多主流大数据框架 从来没有主观意义上去深度地思考过大数据 现在对于大数据我讲讲自己的想法 1 大数据的产生 为什么会有大数据 随着互联网的发展 现在大部分日常生活都通过网络变得十分便捷 吃穿住行等等的日常生活都离不开
  • 【三十九、MySql】进阶篇--存储过程--循环(while、repeat、loop)

    1 while循环 语法 先判定循环是有条件的循环控制语句 满足条件后 再执行循环体中的sql语句 while 条件 do sql语句 end while 练习 传入一个参数n 计算1 2 3 n的值 create procedure p5
  • 卸载centos自带jdk&安装指定版本jdk

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 删除jdk 1 查看java安装情况 2 查看 3 卸载 4 rpm总结 5 安装 1 查看java安装情况 java version 2 查看 rpm qa grep jd
  • js实现贪吃蛇小游戏

  • 2023年电子设计大赛E题——省一设计报告

    2023 年全国大学生电子设计竞赛 运动目标控制与自动追踪系统 E 题 本科组 设计报告 目录 一 系统方案 4 一 主控模块的论证与选择 4 1 方案一 4 2 方案二 5 3 总结 5 二 巡线模块的论证与选择 5 1 方案一 5 2
  • 大数据工程师面试经验

    全中国的IT公司只想去阿里 因为阿里真的是中国程序员的朝圣地 进去真的能学很多 最终经历了5轮面试 顺利拿到了offer 这里和大家分享一下我的面试经验 一面过程 首先是一次不记名面试 这里我也真的很感谢这场不记名面试了 如果没有这场不记名
  • C++类的拷贝(复制)构造函数深入理解

    目录 一 拷贝构造函数的基本了解和使用 二 拷贝构造函数的一些注意事项 三 拷贝构造函数的调用时机 使用一个对象 直接构造 显式构造 或 初始化 隐式构造 另一个同类对象 关于拷贝构造函数与赋值函数的区别与联系 作为函数的形式参数时 作为函
  • 路由器网口1一直闪烁正常吗_路由器灯怎么闪才正常

    在我们日常生活当中 很多人家里都会安装各种各样的路由器 有的人家中安装的路由器是穿墙路由器 无论是安装的哪种路由器都有可能出现一些故障 今天小编就来跟大家说一说路由器灯怎么闪才正常 希望可以给大家带来帮助 感兴趣的朋友们可以跟随小编一起来了
  • vue3+ts动态写后台管理系统左边的菜单栏

    1 在路由router文件里面写路由元信息传递左侧显示的路由 以及路由的名字 const routes Array
  • Pandas知识点-索引和切片操作

    Pandas知识点 索引和切片操作 索引和切片操作是最基本最常用的数据处理操作 Pandas中的索引和切片操作基于Python的语言特性 支持类似于numpy中的操作 也可以使用行标签 列标签以及行标签与列标签的组合来进行索引和切片操作 本
  • Golang架构直通车——理解defer

    文章目录 defer应用 defer触发时机 defer执行顺序 预计算参数 defer实现原理 defer应用 Go 语言的 defer会在当前函数或者方法返回之前执行传入的函数 它会经常被用于关闭文件描述符 关闭数据库连接以及解锁资源