深入理解defer(上)defer基础
深入理解 defer 分上下两篇文章,本文为上篇,主要介绍如下内容:
为什么需要 defer;
defer 语法及语义;
defer 使用要点;
defer 语句中的函数到底是在 return 语句之后被调用还是 return 语句之前被调用。
为什么需要 defer
先来看一段没有使用 defer 的代码:
func f() {
r := getResource() //0,获取资源
......
if ... {
r.release() //1,释放资源
return
}
......
if ... {
r.release() //2,释放资源
return
}
......
if ... {
r.release() //3,释放资源
return
}
......
r.release() //4,释放资源
return
}
f() 函数首先通过调用 getResource() 获取了某种资源(比如打开文件,加锁等),然后进行了一些我们不太关心的操作,但这些操作可能会导致 f() 函数提前返回,为了避免资源泄露,所以每个 return 之前都调用了 r.release() 函数对资源进行释放。这段代码看起来并不糟糕,但有两个小问题:代码臃肿和可维护性比较差。臃肿倒是其次,主要问题在于代码的可维护性差,因为随着开发和维护的进行,修改代码在所难免,一旦对 f() 函数进行修改添加某个提前返回的分支,就很有可能在提前 return 时忘记调用 r.release() 释放资源,从而导致资源泄漏。
那么我们如何改善上述两个问题呢?一个不错的方案就是通过 defer 调用 r.release() 来释放资源:
func f() {
r := getResource() //0,获取资源
defer r.release() //1,注册延迟调用函数,f()函数返回时才会调用r.release函数释放资源
......
if ... {
return
}
......
if ... {
return
}
......
if ... {
return
}
......
return
}
可以看到通过使用 defer 调用 r.release(),我们不需要在每个 return 之前都去手动调用 r.release() 函数,代码确实精简了一点,重要的是不管以后加多少提前 return 的代码,都不会出现资源泄露的问题,因为不管在什么地方 return ,r.release() 函数始终都会被调用。
defer 语法及语义
defer语法很简单,直接在普通写法的函数调用之前加 defer 关键字即可:
defer xxx(arg0, arg1, arg2, ......)
defer 表示对紧跟其后的 xxx() 函数延迟到 defer 语句所在的当前函数返回时再进行调用。比如前文代码中注释 1 处的 defer r.release() 表示等 f() 函数返回时再调用 r.release() 。下文我们称 defer 语句中的函数叫 defer函数。
defer 使用要点
对 defer 的使用需要注意如下几个要点:
延迟对函数进行调用;
即时对函数的参数进行求值;
根据 defer 顺序反序调用;
下面我们用例子来简单的看一下这几个要点。
defer 函数延迟调用
func f() {
defer fmt.Println("defer")
fmt.Println("begin")
fmt.Println("end")
return
}
这段代码首先会输出 begin 字符串,然后是 end ,最后才输出 defer 字符串。
defer 函数参数即时求值
func g(i int) {
fmt.Println("g i:", i)
}
func f() {
i := 100
defer g(i) //1
fmt.Println("begin i:", i)
i = 200
fmt.Println("end i:", i)
return
}
这段代码首先输出 begin i: 100,然后输出 end i: 200,最后输出 g i: 100 ,可以看到 g() 函数虽然在f函数返回时才被调用,但传递给 g() 函数的参数还是100,因为代码 1 处的 defer g(i) 这条语句执行时 i 的值是100。也就是说 defer 函数会被延迟调用,但传递给 defer 函数的参数会在 defer 语句处就被准备好。
反序调用
func f() {
defer fmt.Println("defer01")
fmt.Println("begin")
defer fmt.Println("defer02")
fmt.Println("----")
defer fmt.Println("defer03")
fmt.Println("end")
return
}
这段程序的输出如下:
begin
----
end
defer03
defer02
defer01
可以看出f函数返回时,第一个 defer 函数最后被执行,而最后一个 defer 函数却第一个被执行。
defer 函数的执行与 return 语句之间的关系
到目前为止,defer 看起来都还比较好理解。下面我们开始把问题复杂化
package main import "fmt" var g = 100 func f() (r int) {
defer func() {
g = 200
}() fmt.Printf("f: g = %d\n", g) return g
} func main() {
i := f()
fmt.Printf("main: i = %d, g = %d\n", i, g)
}
输出:
$ ./defer
f: g =100
main: i =100, g =200
这个输出还是比较容易理解,f() 函数在执行 return g 之前 g 的值还是100,所以 main() 函数获得的 f() 函数的返回值是100,因为 g 已经被 defer 函数修改成了200,所以在 main 中输出的 g 的值为200,看起来 defer 函数在 return g 之后才运行。下面稍微修改一下上面的程序:
package main import "fmt" var g = 100 func f() (r int) {
r = g
defer func() {
r = 200
}() fmt.Printf("f: r = %d\n", r) r = 0
return r
} func main() {
i := f()
fmt.Printf("main: i = %d, g = %d\n", i, g)
}
输出:
$ ./defer
f: r =100
main: i =200, g =100
从这个输出可以看出,defer 函数修改了 f() 函数的返回值,从这里看起来 defer 函数的执行发生在 return r 之前,然而上一个例子我们得出的结论是 defer 函数在 return 语句之后才被调用执行,这两个结论很矛盾,到底是怎么回事呢?
仅仅从go语言的角度来说确实不太好理解,我们需要深入到汇编来分析一下。
老套路,使用 gdb 反汇编一下 f() 函数:
0x0000000000488a30<+>: mov %fs:0xfffffffffffffff8,%rcx
0x0000000000488a39<+>: cmp 0x10(%rcx),%rsp
0x0000000000488a3d<+>: jbe 0x488b33 <main.f+>
0x0000000000488a43<+>: sub $0x68,%rsp
0x0000000000488a47<+>: mov %rbp,0x60(%rsp)
0x0000000000488a4c<+>: lea 0x60(%rsp),%rbp
0x0000000000488a51<+>: movq $0x0,0x70(%rsp) # 初始化返回值r为0
0x0000000000488a5a<+>: mov 0xbd66f(%rip),%rax # 0x5460d0 <main.g>
0x0000000000488a61<+>: mov %rax,0x70(%rsp) # r = g
0x0000000000488a66<+>: movl $0x8,(%rsp)
0x0000000000488a6d<+>: lea 0x384a4(%rip),%rax # 0x4c0f18
0x0000000000488a74<+>: mov %rax,0x8(%rsp)
0x0000000000488a79<+>: lea 0x70(%rsp),%rax
0x0000000000488a7e<+>: mov %rax,0x10(%rsp)
0x0000000000488a83<+>: callq 0x426c00 <runtime.deferproc>
0x0000000000488a88<+>: test %eax,%eax
0x0000000000488a8a<+>: jne 0x488b23 <main.f+>
0x0000000000488a90<+>: mov 0x70(%rsp),%rax
0x0000000000488a95<+>: mov %rax,(%rsp)
0x0000000000488a99<+>: callq 0x408950 <runtime.convT64>
0x0000000000488a9e<+>: mov 0x8(%rsp),%rax
0x0000000000488aa3<+>: xorps %xmm0,%xmm0
0x0000000000488aa6<+>: movups %xmm0,0x50(%rsp)
0x0000000000488aab<+>: lea 0x101ee(%rip),%rcx # 0x498ca0
0x0000000000488ab2<+>: mov %rcx,0x50(%rsp)
0x0000000000488ab7<+>: mov %rax,0x58(%rsp)
0x0000000000488abc<+>: nop
0x0000000000488abd<+>: mov 0xd0d2c(%rip),%rax# 0x5597f0 <os.Stdout>
0x0000000000488ac4<+>: lea 0x495f5(%rip),%rcx# 0x4d20c0 <go.itab.*os.File,io.Writer>
0x0000000000488acb<+>: mov %rcx,(%rsp)
0x0000000000488acf<+>: mov %rax,0x8(%rsp)
0x0000000000488ad4<+>: lea 0x31ddb(%rip),%rax # 0x4ba8b6
0x0000000000488adb<+>: mov %rax,0x10(%rsp)
0x0000000000488ae0<+>: movq $0xa,0x18(%rsp)
0x0000000000488ae9<+>: lea 0x50(%rsp),%rax
0x0000000000488aee<+>: mov %rax,0x20(%rsp)
0x0000000000488af3<+>: movq $0x1,0x28(%rsp)
0x0000000000488afc<+>: movq $0x1,0x30(%rsp)
0x0000000000488b05<+>: callq 0x480b20 <fmt.Fprintf>
0x0000000000488b0a<+>: movq $0x0,0x70(%rsp) # r =
# ---- 下面5条指令对应着go代码中的 return r
0x0000000000488b13<+>: nop
0x0000000000488b14<+>: callq 0x427490 <runtime.deferreturn>
0x0000000000488b19<+>: mov 0x60(%rsp),%rbp
0x0000000000488b1e<+>: add $0x68,%rsp
0x0000000000488b22<+>: retq
# ---------------------------
0x0000000000488b23<+>: nop
0x0000000000488b24<+>: callq 0x427490 <runtime.deferreturn>
0x0000000000488b29<+>: mov 0x60(%rsp),%rbp
0x0000000000488b2e<+>: add $0x68,%rsp
0x0000000000488b32<+>: retq
0x0000000000488b33<+>: callq 0x44f300 <runtime.morestack_noctxt>
0x0000000000488b38<+>: jmpq 0x488a30 <main.f>
f() 函数本来很简单,但里面使用了闭包和 Printf,所以汇编代码看起来比较复杂,这里我们只挑重点出来说。f() 函数最后 2 条语句被编译器翻译成了如下6条汇编指令:
0x0000000000488b0a<+>: movq $0x0,0x70(%rsp) # r =
# ---- 下面5条指令对应着go代码中的 return r
0x0000000000488b13<+>: nop
0x0000000000488b14<+>: callq 0x427490 <runtime.deferreturn> # deferreturn会调用defer注册的函数
0x0000000000488b19<+>: mov 0x60(%rsp),%rbp # 调整栈
0x0000000000488b1e<+>: add $0x68,%rsp # 调整栈
0x0000000000488b22<+>: retq # 从f()函数返回
# ---------------------------
这6条指令中的第一条指令对应到的go语句是 r = 0,因为 r = 0 之后的下一行语句是 return r ,所以这条指令相当于把 f() 函数的返回值保存到了栈上,然后第三条指令调用了 runtime.deferreturn 函数,该函数会去调用我们在 f() 函数开始处使用 defer 注册的函数修改 r 的值为200,所以我们在main函数拿到的返回值是200,后面三条指令完成函数调用栈的调整及返回。
从这几条指令可以得出,准确的说,defer 函数的执行既不是在 return 之后也不是在 return 之前,而是一条go语言的 return 语句包含了对 defer 函数的调用,即 return 会被翻译成如下几条伪指令
保存返回值到栈上
调用defer函数
调整函数栈
retq指令返回
到此我们已经知道,前面说的矛盾其实并非矛盾,只是从Go语言层面来理解不好理解而已,一旦我们深入到汇编层面,一切都会显得那么自然,正所谓汇编之下了无秘密。
总结
defer 主要用于简化编程(以及实现 panic/recover ,后面会专门写一篇相关文章来介绍)
defer 实现了函数的延迟调用;
defer 使用要点:延迟调用,即时求值和反序调用;
go 语言的 return 会被编译器翻译成多条指令,其中包括保存返回值,调用defer注册的函数以及实现函数返回。
本文我们主要从使用的角度介绍了defer 的基础知识,下一篇文章我们将会深入 runtime.deferproc 和 runtime.deferreturn 这两个函数分析 defer 的实现机制。
深入理解defer(上)defer基础的更多相关文章
- 深入理解gradle编译-Android基础篇
深入理解gradle编译-Android基础篇 导读 Gradle基于Groovy的特定领域语言(DSL)编写的一种自动化建构工具,Groovy作为一种高级语言由Java代码实现,本文将对Gradle ...
- MindSpore技术理解(上)
MindSpore技术理解(上) 引言 深度学习研究和应用在近几十年得到了爆炸式的发展,掀起了人工智能的第三次浪潮,并且在图像识别.语音识别与合成.无人驾驶.机器视觉等方面取得了巨大的成功.这也对算法 ...
- ZT 理解 Android 上的安全性
理解 Android 上的安全性 http://www.ibm.com/developerworks/cn/xml/x-androidsecurity/ 利用沙箱.应用程序签名和权限增强应用程序安全性 ...
- 基于TensorRT的BERT实时自然语言理解(上)
基于TensorRT的BERT实时自然语言理解(上) 大规模语言模型(LSLMs)如BERT.GPT-2和XL-Net为许多自然语言理解(NLU)任务带来了最先进的精准飞跃.自2018年10月发布以来 ...
- 计算机网络之HTTP(上)基础知识点
计算机网络,应该是我们编程开发.产品上线到正常的运行维护需要考虑的基本条件之一.之前我记录了一篇很简单的计算机的组成(http://www.cnblogs.com/zhangxiongcn/p/636 ...
- Go 初体验 - 令人惊叹的语法 - defer.4 - defer 对宿主函数返回值的影响
defer 函数可以影响宿主函数的返回值 看代码: 调用: 输出: 结果又让人意外了. coo1:因为传引用,return 时 i = 100, return 返回的也是 100,return 执行之 ...
- Effective Java通俗理解(上)
这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...
- 爬虫基础---HTTP协议理解、网页的基础知识、爬虫的基本原理
一.HTTP协议的理解 URL和URI 在学习HTTP之前我们需要了解一下URL.URI(精确的说明某资源的位置以及如果去访问它) URL:Universal Resource Locator 统一资 ...
- Nodejs第一天-{Nodejs基础 深刻理解浏览器 环境变量 基础语法}
Nodejs第一天 1.什么是Nodejs Nodejs是一个可以运行(解析)ECMAScript的环境; ECMAScript是规定了一些列的语法 ,这些语法想要解析的执行就需要放在某个环境 ...
- 理解Java中对象基础Object类
一.Object简述 源码注释:Object类是所有类层级关系的Root节点,作为所有类的超类,包括数组也实现了该类的方法,注意这里说的很明确,指类层面. 所以在Java中有一句常说的话,一切皆对象, ...
随机推荐
- python3.5.3rc1学习九:正则表达式
# 正则表达式 ''''' 正则表达式是有一些特殊字符组成,能够帮你找到一些符合一定规则的字符串 先来了解几个符号所代表的意思 \d 匹配所有的数字 \D 匹配所有,但是数字除外 \s 空格 \S 匹 ...
- Jupyter notebook and Octave kernel installation
Jupyter notebook 安装 为了更加方便地写 Python 代码,还需要安装 Jupyter notebook. 利用 pip 安装 Jupyter notebook. 为什么要使用 Ju ...
- django-订单并发处理--悲观锁和乐观锁
冲突比较少的时候,使用乐观锁. 冲突比较多的时候,使用悲观锁. (1) 悲观锁 select * from df_goods_sku where id=17 for update; 悲观锁获取 ...
- Nginx Rewrite域名及资源重定向
一.正则匹配 1.匹配规则 ^$ 标识符匹配后面跟-一个字符串.匹配字符串后将停止对后续的正则表达式进行匹配,如location ^~/images/,在匹配了/images ...
- asp.net mvc 系统操作日志设计
第一步.系统登录日志 通过signalr来管理用户的登录情况,并保存用户的登录记录. 第二步 通过mvc过滤器,来横切路由访问记录. 保存方式:通过httpclient异步请求webapi 数据通过m ...
- Paper | Dynamic Residual Dense Network for Image Denoising
目录 1. 故事 2. 动机 3. 做法 3.1 DRDB 3.2 训练方法 4. 实验 发表于2019 Sensors.这篇文章的思想可能来源于2018 ECCV的SkipNet[11]. 没开源, ...
- [分布式学习]消息队列之rocketmq笔记
文档地址 RocketMQ架构 哔哩哔哩上的视频 mq有很多,近期买了<分布式消息中间件实践>这本书,学习关于mq的相关知识.mq大致有有4个功能: 异步处理.比如业务端需要给用户发送邮件 ...
- HTML+CSS基础 border css属性 Div块 盒子
border css属性 边框颜色 border-color:red/#ffffff/rgb()默认为黑色 边框样式 border-style:solid (实线) dashed (虚线).默认为n ...
- mysql8 安装
准备工作: 首先安装这些依赖 yum install -y flex yum install gcc gcc-c++ cmake ncurses ncurses-devel bison libaio ...
- python 统计使用技巧
python 统计使用技巧 # 1.不输入回车获取值 注:需要tty模块配合. fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) ...