Golang中的defer关键字的用法、原理以及它的坑

【Golang】defer的使用和底层实现 defer是Go语言的关键字,一般用于资源的释放和异常的捕捉(比如:文件打开、加锁、数据库连接、异常捕获) 阅读详情

延迟函数调用(deferred function call)是golang中很有特点的一个功能,通过defer修饰的函数调用会在函数退出的时候才被真正调用,它可以用来进行资源释放等收尾工作。
一个普通的函数调用被defer关键字修饰以后,就构成了一个延迟函数调用,和协程调用类似,被延迟的函数调用的所有返回值都会全部被舍弃。

延迟函数调用的用法

首先我们看看延迟函数调用的用法,它的用法其实很简单!
简单是示例代码:

func main(){
  defer func() { fmt.Println("延迟调用") }()
  fmt.Println("正常调用")
}

代码执行结果:

正常代码
延迟调用

从结果我们可以很直观的发现,被defer修饰的函数调用在后面执行了,也就是被延迟调用了。

多个延迟函数调用的执行顺序

上面的示例代码很简单,输出结果也很好理解,接下来我们看一个稍微复杂一丢丢的代码:

func main(){
	defer func() { fmt.Println("延迟调用1") }()
	defer func() { fmt.Println("延迟调用2") }()
	fmt.Println("正常代码")
}

这段代码和之前的代码唯一的不同之处是多了一个被defer修饰的函数调用,那么这小段代码的输出又会是什么样的呢?
是先输出[延迟调用1]还是先输出[延迟调用2]呢?跑一下程序自然就知道啦!
代码执行结果:

正常代码
延迟调用2
延迟调用1

[延迟调用2]被先输出了,这是为什么呢?
这是因为每一个协程都会维护一个延迟调用堆栈,按照代码顺序把需要延迟调用的函数压入栈中,当函数进入退出阶段后,就会从延迟调用堆栈中取出需要执行的函数调用并执行。

延迟函数调用的实现原理

上面讲了基本的用法,接下来我们再深入到golang源码看看defer关键字是如何实现的。为了便于分析,我用下面的代码进行分析:

func main(){
	defer func() {}()
	defer func() {}()
}

然后执行如下命令看看这小段代码的执行过程:

GOSSAFUNC=main go build main.go

我们可以得到如下的ssa执行过程:
在这里插入图片描述
看不懂?没关系!会找关键信息就ok了!通过ssa执行过程我们可以发现defer命令被翻译成了runtime.deferproc命令,并且在退出阶段会调用runtime.deferreturn命令。
首先我们看看deferproc命令的源码:

func deferproc(siz int32, fn *funcval) {
  gp := getg()
  // ...
  // ...
	d := newdefer(siz)
  // ...
  // ...
}
func newdefer(siz int32) *_defer {
  // ...
  // ...
	d = (*_defer)(mallocgc(total, deferType, true))
  // ...
  // ...
	d.siz = siz
	d.link = gp._defer
	gp._defer = d
	return d
}

上面的源码省略了一些细节,留下了主干。deferproc命令会创建一个_defer类型的数据,它实际上是一个链表类型,有一个字段link指向下一个_defer数据。在当前协程上会保存这个_defer链表的头部,每次创建defer的时候就会插入到协程_defer链表的头部,形成一个基于链表的堆栈。
然后我们再看看deferreturn命令的源码:

func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
  // ...
  // ...
	fn := d.fn
	d.fn = nil
	gp._defer = d.link
	freedefer(d)
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}

从上面的代码我们可以看出,当我们进入退出阶段,执行deferreturn命令的时候,会从当前协程的_defer链表中取出头部(前面介绍了是如何往里面存放数据的),并把下一个元素作为_defer链表的头部,然后再使用jmpdefer指令完成跳转调用,jmpdefer完全是使用汇编完成的了,并且各个平台实现的代码也不一样,这里就不再继续分析了。
上面两个部分分析了defer关键字的主要实现逻辑,从中我们可以知道如下的特定:

  • 多个defer修饰的延迟调用函数是存放在堆栈中的,满足先进后出的调用规则。
  • jmpdefer进行代码跳转并不是普通的函数调用,处于延迟调用堆栈中的任意两个调用之间也不存在调用关系

带参数的延迟函数调用

上面的示例介绍的延迟函数调用都是不带参数的,实际上延迟函数调用是可以带参数的。比如下面的代码,你们知道它的输出吗,知道为什么是这样的输出吗?

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

我们先跑一跑这段代码,看看它的的输出结果,然后再慢慢分析原因:

3
3
3

2
1
0

第一个for循环中的defer都是带参数的延迟调用,我们可以发现它会记录deferproc命令执行那一刻的变量值。第二个for循环是在延迟函数中使用变量,可以发现它使用的值是for循环完以后的值。
规则很简单,那么是怎么实现的呢?
细心的朋友会发现上面的defer关键字实现源码已经涉及到了这一块的内容。deferproc命令的第一个参数接收一个int32值,它保存了延迟函数的参数内存大小,如果延迟函数有参数,它就会大于0,如果延迟函数没有参数这个值就会为0。前面为了更加直观的分析延迟函数堆栈的逻辑,省略了部分代码,下面这段复制参数的代码也是在deferproc命令中实现的

func deferproc(siz int32, fn *funcval) {
  // ...
  // ...
  argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
  // ...
  // ...
  switch siz {
  case 0:
    // Do nothing.
  case sys.PtrSize:
    *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
  default:
    memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
  }
  // ...
  // ...
}

这段代码的作用就是把延迟函数的参数保存在defer对象中,在退出阶段使用jmpdefer进行调用的时候,会把这个参数再传给被调用的延迟函数。
通过上面的分析我们可以知道defer具有下面这些特点:

  • 带参数的延迟调用,它的实参是在此调用被推入延迟调用堆栈的时候被估值的,而不是延迟函数被调用的时刻。

延迟函数使用中的坑

前面介绍了defer的基本用法和实现原理,下面我们再介绍以下它在平时使用过程中可能遇到的坑。

  • 匿名返回值和命名返回值在延迟函数中使用的表现是不一样的
    有时候我们希望在延迟调用函数中对返回值进行修改,这个时候我们就必须特别注意一个点了:延迟函数中对匿名返回值的修改有可能会不成功!
    以下面的代码为例:
func test1()(k string){
	defer func() {
		k = "defer value"
	}()
	k = "normal value"
	return
}

func test2()string{
	k := "normal value"
	defer func() {
		k = "defer value"
	}()
	return k
}

func main(){
	fmt.Println("test1 result: " + test1())
	fmt.Println("test2 result: " + test2())
}

这段代码的执行结果是:

test1 result: defer value
test2 result: normal value

也就是说如果想要在延迟函数中修改返回值,就必须使用命名返回值,匿名返回值是无法在延迟函数中进行修改的!
这是为什么呢?
其实查看上面代码的ssa执行过程就能够很快的找到原因了,总结起来就是:
1、每一个函数都会有一个返回值表,这个表中会保存所有的返回值内容。
2、命名返回值会在进入函数的时候就在返回值表上初始化好相应的返回值,上面示例test1函数中使用的变量k实际上是返回值表中的变量。
3、匿名返回值在进入函数的时候不会初始化返回值表上的值,return的时候才会拷贝数据到返回值表中,上面示例test2函数中使用的变量k只是一个临时变量,并不是返回值表中的变量。
4、匿名返回值的情况,return时临时变量拷贝到返回值表发生在调用延迟函数之前,这就解释了为什么延迟函数中修改k的值并不能影响返回值。命名返回值的情况由于一直使用的都是返回值表中的变量k,所以延迟函数中可以修改返回值。

  • 调用os.Exit时defer不会被执行

正常退出函数时,该函数中压入延迟调用栈的延迟函数会被调用。通过panic退出函数时,协程的延迟调用栈上的所有延迟调用函数都会被调用,除非某个延迟函数中调用了recover。os.Exit退出的话就不会执行任何延迟调用了,因为这个时候整个程序都退出了,所以没事不要直接调用os.Exit退出程序,这种操作可能导致各种收尾工作得不到执行。

  • defer中使用闭包函数的时候,只有最后一次调用是被延迟执行的

有时候为了代码更加的简洁,我们会用defer来修饰连续的函数调用过程,这个时候需要特别注意,并不是这一串的函数调用都会被延迟执行的!
以如下代码为例:

func test() func(){
	fmt.Println("1")
	return func(){
		fmt.Println("2")
	}
}

func main(){
	fmt.Println("a")
	defer test()()
	fmt.Println("b")
}

他的输出结果如下:

a
1
b
2

所以defer修饰的延迟函数如果存在连续调用的,一定要注意只有一次调用是延迟的!!!

59、掌握Go语言延迟执行:defer关键字的实战技巧 本文深入探讨了Go语言中独特的`defer`关键字,详细介绍了其工作原理、常见用途、实战技巧及性能考量。文章通过丰富案例展示了`defer`在资源管理、异常处理、数据排序和并发编程中的重要作用,同时提醒读者在性能敏感场景下合理使用`defer`,避免不必要的性能开销。本文旨在帮助读者更好地理解和应用`defer`,提升Go编程效率和代码质量。 阅读详情

相关推荐

Go defer 原理剖析篇 — 搞懂 golang defer 所有的使用姿势

文章目录defer 的特性延迟调用LIFO作用域异常场景使用姿势并发同步锁场景资源释放panic-recover总结 更多干货:关注公众号 奇伢云存储 Golang 的 defer 是什么?通俗来讲就是延迟调用。defer 会在当前函数返回之前执行 defer 注册的函数。比如 defer func_x( ) 这样语句会让你注册一个函数变量到 defer 的全局链表中,在 defer 语句所在的函数退出之前调用。 笔者使用一段时间 Golang 之后,对 Golang defer 的理解认为作用有两点

伢之国 1010

Golang中defer的实现原理

前言 在Go语言中,可以使用关键字defer向函数注册退出调用,即主函数退出时,defer后的函数才被调用。defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。 所以,defer后面的函数通常又叫做延迟函数 defer规则 1.延迟函数的参数在defer语句出现时就已经确定下来了 func a() { i := 0 defer fmt.Println(i) i++ return } 返回结果: defer语句中打印的变量i在defer出现时就已经拷

香烟,瓜子,矿泉水 1187

Go Defer 关键字原理浅析

Go 关键字- Defer defer defer 修饰的函数是一个延迟函数,在包含它的函数返回时运行 type _defer struct { siz int32 // 由deferproc第一个参数传入,参数和结果的内存大小 started bool // 标识defer函数是否已经开始执行 heap bool // 堆分配、栈分配 openDefer bool //表示当前 defer 是否经过开放编码的优化 sp uintptr // 栈

王小明的专栏 484

GoLang之defer底层系列一(v 1.12)

GoLang之defer底层系列一(v 1.12)

weixin_52690231的博客 627

手把手教你用mitmproxy抓包抖音数据流(附Python脚本模板)

本文详细介绍了如何使用mitmproxy抓包抖音数据流,包括环境搭建、证书安装和Python脚本模板。从mitmproxy配置到抖音API解析,提供完整的移动端数据流分析方案,帮助开发者快速掌握数据抓取技术,适用于应用优化、竞品分析和安全审计等场景。

weixin_33670786的博客 308

[Python]:热血传奇-Wzl,Wzx的解析

首先我们要了解,wzl与wzx的对应关系,wzx里面存的是每张二进制图片数据在wzl中的偏移量,所以取到wzx中的偏移数据就可以取到wzl每张图片数据,其实就这么简单。结构体为 [Byte][Byte][Byte][Byte][short][short][short][short][int]-读取44至48字节代表为,[wzx中偏移值总数量] [int类型] [4字节]-根据在wzx中取得的偏移值总数量,每张图片偏移值在wzl中取图片数据。2.[取wzl中图片偏据]:根据wzx中取得数据对[

qq_36138506的博客 3447

Go语言defer关键字

go语言的defer关键字用于延迟调用,下面是关于go语言defer关键字的一些基础概念: 1. defer关键字用于注册延迟调用; 2. 这些调用到直到包含当前该defer关键字的函数执行完了才会被执行; 3. 如果定义了多个defer语句,按照先进后出的方式执行,也即后定义的defer语句会先被执行,这也比较好理解,如果之前的资源释放掉了,后面需要使用掉之前资源的语句就没法执行了; 4. defer语句中的变量,在defer声明的时候就决定了,也即在defer语句中使用到的变量的值为在defer

二哈 1306

手把手教你用Simulink搭建IEEE33节点配电网模型(附赠两本电力系统书籍)

本文详细介绍了如何使用Simulink搭建IEEE33节点配电网模型,涵盖从基础配置到高级应用的完整流程。通过MATLAB脚本自动化参数注入、监测系统部署和典型应用场景实战,帮助读者快速掌握配电仿真模型的核心技术。附赠两本电力系统经典书籍资源,助力深入学习与研究。

weixin_29073241的博客 249

一. go 常见控制结构实现原理之 defer

查看defer底层结构体,实际是一个FIFO链表, 多个defer时会用内部的link连接,使用defer有三条规则规则一:延迟函数的参数在defer语句出现时就已经确定下来了规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行规则三:延迟函数可能操作主函数的具名返回值。

qq_29799655的博客 329

SOEM主站开发篇(1):移植SOEM主站到Linux开发板

详细介绍了SOEM主站源码移植到Linux开发板上的步骤,同时演示了如何使用Linux开发板获取EtherCAT从站的PDO映射信息。

kevin1499的博客 2398

PointNet++改进策略 :模块改进 | x-Conv | PointCNN, 结合局部结构与全局排列提升模型性能

这篇论文介绍了一种名为PointCNN的方法,旨在从点云(point cloud)数据中学习特征。传统卷积神经网络(CNN)在处理规则网格数据(如图像)时非常有效,但由于点云是无序且不规则的,直接在其上应用卷积操作会导致形状信息丢失,并对点的排列顺序敏感。为了解决这一问题,论文提出了一种X-Conv操作。该方法通过学习一种来重新排列点云中的点并加权输入特征。然后,将典型的卷积操作应用于转换后的特征。这种方法将传统的 CNN 扩展到点云特征学习,并命名为PointCNN。

u012901740的博客 2300

Golang defer 关键字实现原理

这里写目录标题1. Golang defer 关键字实现原理1.1. 作用1.2. defer 的实现 1. Golang defer 关键字实现原理 1.1. 作用 defer 用于资源的释放, 会在函数返回之前进行调用。 如果有多个 defer 表达式, 调用顺序类似于栈, 越后面的 defer 表达式越先被调用。 defer 是在 return 之前执行的。这个在官方文档中是明确说明了的。要使用 defer 时不踩坑, 最重要的一点就是要明白, return xxx 这一条语句并不是一条原子指令! 函

云满笔记 281

28、数字电路测试与半导体内存测试全解析

本文全面解析了数字电路中的顺序电路测试生成方法与半导体内存测试技术。内容涵盖顺序电路中的故障检测、多重观察测试、可驱动性分析、伪组合电路测试及初始化问题,并深入探讨了内存技术发展、故障模型、测试流程与未来趋势。重点介绍了DRAM、SRAM等主流内存类型的特点,以及制造和使用过程中面临的永久性与非永久性故障挑战,提出了基于故障模型的高效测试策略与优化方案。

go5gopher的博客 122

Go的defer原理

Go的defer原理

热爱C++和golang的学生 1350

9.6华为OD机试真题 新系统 - 仓储货物有序出库 (Java/Py/C/C++/Js/Go)

9.6华为OD机试真题 新系统 - 仓储货物有序出库 (Java/Py/C/C++/Js/Go)

算法大师! 1201

YOLOv11改进策略【模型轻量化】| 替换骨干网络 CVPR-2023 FasterNet 高效快速的部分卷积块

本文记录的是基于FasterNet的YOLOv11轻量化改进方法研究。FasterNet的网络结构借鉴 CNN 的设计理念,通过提出的PConv减少推理时的计算和内存成本,同时减少通道数并增加部分比例,降低延迟,并通过后续的PWConv来弥补特征信息可能缺失的问题,提高了准确性。本文在替换骨干网络中配置了原论文中的和六种模型,以满足不同的需求。

Limiiiing的博客 2030

Golang中defer关键字实现原理

defer和go一样都是Go语言提供的关键字。defer用于资源的释放,会在函数返回之前进行调用。一般采用如下模式: f,err := os.Open(filename) if err != nil { panic(err) } defer f.Close() 如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。 ...

skh2015java的博客 3147

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Go语言之defer(原理、常见的坑)

defer的原理、defer的坑

Sheena997的博客 6501

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

深入理解 Go 语言中的 defer 关键字

defer关键字是 Go 语言中强大而灵活的特性,通过延迟调用机制,为资源管理、异常处理和程序流程控制提供了优雅的解决方案。了解defer的作用、特点、注意事项以及底层原理,有助于我们更好地运用这一特性,编写出高效、可靠的 Go 程序。在实际编程中,合理使用defer可以显著提升代码的质量和可维护性,避免资源泄露和程序异常导致的崩溃等问题,是每个 Go 开发者必备的技能之一。希望本文对大家深入理解defer有所帮助,在日常编程中能够更加熟练地运用这一特性。

m0_57836225的博客 3014

Go语言中的defer关键字

官方文档中关于defer语句的解释: defer语句延迟执行一个函数,该函数被推迟到当包含它的程序返回时(包含它的函数 执行了return语句/运行到函数结尾自动返回/对应的goroutine panic)执行。 每次defer语句执行时,defer修饰的函数的返回值和参数取值会照常进行计算和保存,但是该函数不会执行。等到上一级函数返回前,会按照defer

Han的小站 7716

go中的关键字-defer

1. defer的使用   defer 延迟调用。我们先来看一下,有defer关键字的代码执行顺序: func main() { defer func() { fmt.Println("1号输出") }() defer func() { fmt.Println("2号输出") }() }   输出结果: 2号出来 1号出来...

dz45693的专栏 2752

go-defer的用法及执行原理

Go 中有几个比较特殊的关键字,如 defer,尤其 defer+panic+recover的组合可以发挥出 java 中 try...catch...fanilly 的作用,功能非常强大,值的去深入学习。同时他们每个又有自身的特性,这一节我们先来理解一下 defer。 defer 关键字在 go 中的使用率算是非常高的,类似于 finally 与 析构函数的作用,用来做方法的善后工作。 1. 怎么使用? defer 后面会接受一个函数,但该函数不会立刻被执行,而是等到包含它的程序返回时(包含它的函数执行了

代码能让我保持心静,心静能让我保持动力 2085

【go】go中defer实现原理

deferdefer。

m0_74282926的博客 1293

Golang原理剖析(defer、defer面试与分析)

Go语言defer机制解析 defer是Go语言的关键字,用于将函数调用延迟到当前函数返回或panic时执行。其特点包括: 后进先出:多个defer按逆序执行 实现方式:编译器通过生成_defer结构体实现延迟调用 内存分配:优先使用开放编码,其次根据逃逸分析决定栈/堆分配 底层结构:_defer结构体包含函数指针、调用信息等,以链表形式组织 执行时,编译器会插入deferproc/deferprocStack(分配)和deferreturn(执行)函数。优化后减少了堆分配,提升了性能。

Atticus的博客 772

Golang关键字defer

Golang关键字defer

嘻嘻爱编码的博客 391

Golang defer解读

defer defer是Go语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过return正常结束或者panic导致的异常结束)执行。 defer语句通常用于一些成对操作的场景: 打开链接/关闭链接 加锁/释放锁 打开文件/关闭文件 defer在一些需要回收资源的场景非常有用,可以很方便地在函数结束前做一些清理操作。在打开资源语句的下一行,直接一句def...

王佳宇的个人博客 6078
上一篇: 聊聊几种基本限流算法
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值