热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Golang架构直通车——理解defer

文章目录defer应用defer触发时机defer执行顺序预计算参数defer实现原理defer应用Go语言的defer会在当前函数或者方法返回之前执行传入的函数。它会经常被用于


文章目录

  • defer应用
    • defer触发时机
    • defer执行顺序
    • 预计算参数
  • defer实现原理


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 :&#61; 0; i < 5; i&#43;&#43; {defer fmt.Println(i)}
}

其输出为&#xff1a;

4 3 2 1 0

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

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

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 传入的函数不是在退出代码块的作用域时执行的&#xff0c;它会在当前函数和方法返回之前被调用。


预计算参数

假设我们想要计算 main 函数运行的时间&#xff0c;可能会写出以下的代码&#xff1a;

func main() {startedAt :&#61; time.Now()defer fmt.Println(time.Since(startedAt))time.Sleep(time.Second)
}

$ go run main.go
0s

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

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

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

func main() {startedAt :&#61; time.Now()defer func() { fmt.Println(time.Since(startedAt)) }()time.Sleep(time.Second)
}

$ go run main.go
1s

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


defer实现原理

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

type _defer struct {siz int32started boolsp uintptrpc uintptrfn *funcval_panic *_paniclink *_defer
}

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


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

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

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

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

对于defer关键字&#xff0c;主要有3个函数&#xff1a;


  1. deferproc。在每遇到一个defer关键字时&#xff0c;实际上都会转换为deferproc函数&#xff0c;deferproc函数的作用是将defer函数存入链表中
  2. deferreturn。在return指令前调用&#xff0c;从链表中取出defer函数并执行
  3. deferprocStack。go1.13后对defer做的优化&#xff0c;通过利用栈空间提高效率。

  • 编译期&#xff1b;
    • 将 defer 关键字被转换 runtime.deferproc&#xff1b;
    • 在调用 defer 关键字的函数返回之前插入 runtime.deferreturn&#xff1b;
  • 运行时&#xff1a;
    • runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头&#xff1b;
    • runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行&#xff1b;

如果要了解详情&#xff0c;参考&#xff1a;理解Go语言defer关键字的原理


推荐阅读
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何利用Apache与Nginx高效实现动静态内容分离
    如何利用Apache与Nginx高效实现动静态内容分离 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 在 Linux 环境下,多线程编程是实现高效并发处理的重要技术。本文通过具体的实战案例,详细分析了多线程编程的关键技术和常见问题。文章首先介绍了多线程的基本概念和创建方法,然后通过实例代码展示了如何使用 pthreads 库进行线程同步和通信。此外,还探讨了多线程程序中的性能优化技巧和调试方法,为开发者提供了宝贵的实践经验。 ... [详细]
  • 数字图书馆近期展出了一批精选的Linux经典著作,这些书籍虽然部分较为陈旧,但依然具有重要的参考价值。如需转载相关内容,请务必注明来源:小文论坛(http://www.xiaowenbbs.com)。 ... [详细]
  • Android中将独立SO库封装进JAR包并实现SO库的加载与调用
    在Android开发中,将独立的SO库封装进JAR包并实现其加载与调用是一个常见的需求。本文详细介绍了如何将SO库嵌入到JAR包中,并确保在外部应用调用该JAR包时能够正确加载和使用这些SO库。通过这种方式,开发者可以更方便地管理和分发包含原生代码的库文件,提高开发效率和代码复用性。文章还探讨了常见的问题及其解决方案,帮助开发者避免在实际应用中遇到的坑。 ... [详细]
  • 2012年9月12日优酷土豆校园招聘笔试题目解析与备考指南
    2012年9月12日,优酷土豆校园招聘笔试题目解析与备考指南。在选择题部分,有一道题目涉及中国人的血型分布情况,具体为A型30%、B型20%、O型40%、AB型10%。若需确保在随机选取的样本中,至少有一人为B型血的概率不低于90%,则需要选取的最少人数是多少?该问题不仅考察了概率统计的基本知识,还要求考生具备一定的逻辑推理能力。 ... [详细]
  • 综合实训 201521440015
    Chinesepeople’publicsecurityuniversity网络对抗技术实验报告实验五综合渗透学生姓名常泽远年级15区队4指导教师高见信息技术与网络安全学院2018 ... [详细]
  • 本文详细解析了 Android 系统启动过程中的核心文件 `init.c`,探讨了其在系统初始化阶段的关键作用。通过对 `init.c` 的源代码进行深入分析,揭示了其如何管理进程、解析配置文件以及执行系统启动脚本。此外,文章还介绍了 `init` 进程的生命周期及其与内核的交互方式,为开发者提供了深入了解 Android 启动机制的宝贵资料。 ... [详细]
  • Nginx 反向代理配置与应用指南
    本文详细介绍了 Nginx 反向代理的配置与应用方法。首先,用户可以从官方下载页面(http://nginx.org/en/download.html)获取最新稳定版 Nginx,推荐使用 1.14.2 版本。下载并解压后,通过双击 `nginx.exe` 文件启动 Nginx 服务。文章进一步探讨了反向代理的基本原理及其在实际应用场景中的配置技巧,包括负载均衡、缓存管理和安全设置等,为用户提供了一套全面的实践指南。 ... [详细]
  • 解决针织难题:R语言编程技巧与常见错误分析 ... [详细]
  • 修复一个 Bug 竟耗时两天?真的有那么复杂吗?
    修复一个 Bug 竟然耗费了两天时间?这背后究竟隐藏着怎样的复杂性?本文将深入探讨这个看似简单的 Bug 为何会如此棘手,从代码层面剖析问题根源,并分享解决过程中遇到的技术挑战和心得。 ... [详细]
  • 深入解析OSI七层架构与TCP/IP协议体系
    本文详细探讨了OSI七层模型(Open System Interconnection,开放系统互连)及其与TCP/IP协议体系的关系。OSI模型将网络通信过程划分为七个层次,每个层次负责不同的功能,从物理层到应用层逐步实现数据传输和处理。通过对比分析,本文揭示了OSI模型与TCP/IP协议在结构和功能上的异同,为理解现代网络通信提供了全面的视角。 ... [详细]
  • Windows环境下详细教程:如何搭建Git服务
    Windows环境下详细教程:如何搭建Git服务 ... [详细]
  • 内网渗透技术详解:PTH、PTT与PTK在域控环境中的应用及猫盘内网穿透配置
    本文深入探讨了内网渗透技术,特别是PTH、PTT与PTK在域控环境中的应用,并详细介绍了猫盘内网穿透的配置方法。通过这些技术,安全研究人员可以更有效地进行内网渗透测试,解决常见的渗透测试难题。此外,文章还提供了实用的配置示例和操作步骤,帮助读者更好地理解和应用这些技术。 ... [详细]
author-avatar
漠然粉蔷薇778
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有