defer使用小结
defer
前言
defer作为go里面一个延迟调用的机制,它的存在能够大大的帮助我们优化我们的代码结构。但是我们要弄明白defer的使用机制,不然我们的程序会发生很多莫名的问题。
defer的定义
defer用于延迟指定的函数,只能出现在函数的内部,由defer关键字以及指针对某个函数的调用表达式组成。这里被调用的函数称为延迟函数。
defer执行的规则
- 当外围函数中的语句执行完毕之时,只有当其中所有的延迟函数都执行完毕,外围函数才会真正的执结束执行。
- 当执行外围函数的return语句时,只有其中所有的延迟函数都执行完毕后,该外围函数才会真正的返回。
- 当外围函数中的代码引起运行恐慌时,只有当其中所有的延迟函数调用到都执行完毕后,该运行恐慌才会真正被扩散至调用函数。
为什么需要defer
程序员在编程的时候,经常需要打开一些资源,比如数据库连接、文件、锁等,这些资源需要在用完之后释放掉,否则会造成内存泄漏。
但是程序员都是人,是人就会犯错。因此经常有程序员忘记关闭这些资源。Golang直接在语言层面提供defer关键字,在打开资源语句的下一行,就可以直接用defer语句来注册函数结束后执行关闭资源的操作。因为这样一颗“小小”的语法糖,程序员忘写关闭资源语句的情况就大大地减少了。
但是,defer并不是非常完美的,defer会有小小地延迟,对时间要求特别特别特别高的程序,可以避免使用它,其他一般忽略它带来的延迟。
当然defer也是不能滥用的,比如下面的
i := 0
rw.Lock()
i = 2
defer rw.Unlock()
rw.Lock()
i = 6
defer rw.Unlock()
fmt.Println(i)
defer是在函数退出的时执行的,所以第二个锁,去获取锁的时候,第一个锁还没有释放,所以就报错了。当然这是滥用造成的,我们应该去掉defer
defer进阶
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.
翻译一下:每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来;当外层函数(非代码块,如一个for循环)退出时,defer函数按照定义的逆序执行;如果defer执行的函数为nil, 那么会在最终调用函数的产生panic.
defer语句并不会马上执行的,而是会进入到一个栈,函数return之前,会按照先后顺序执行。造成的结果就是,先定义的函数最后才会被执行。当然,这样的设计也是有理由的,后面定义的函数,可能会需要前面定义的函数的资源,如果前面的函数先执行了,后面函数所需要的依赖可能就不存在了。
我们来看下defer的数据结构:
type _defer struct {
siz int32
started bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
- siz:所有传入参数的总大小
- started:该 defer 是否已经执行过
- sp:函数栈指针寄存器,一般指向当前函数栈的栈顶
- pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令
- fn:指向传入的函数地址和参数
- _panic:指向 _panic 链表
- link:指向 _defer 链表
runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。当有新的_defer被获取,它都会被追加到所在的 Goroutine _defer 链表的最前面。defer 关键字插入时是从后向前的,而 defer 关键字执行是从前向后的,而这就是后调用的 defer 会优先执行的原因。
defer对函数的定义时,对外部的引用方式有两种方式,分别是作为函数参数和作为闭包。当然不管是什么形式,defer执行的时候
都是先把前的值保存起来,然后在最后执行调用链的时候,逐个输出。作为函数参数,则在defer定义时就把值传递给defer,并被
cache起来;作为闭包引用的话,则会在defer函数真正调用时根据整个上下文确定当前的值。
那么如何判断是函数还是闭包呢?
有一句话总结的很好
闭包捕获的变量和常量是引用传递不是值传递
闭包是由函数及其相关引用环境组合而成的实体
闭包=函数+引用环境
所以总结下就是闭包在发生函数调用时,里面的参数发生的是引用传递,而不是值传递。这样从形式上看,go中的匿名函数都是闭包。
作为匿名函数
举个例子
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
打印下输出
3
3
3
这个打印的输出全是3,这就是典型的闭包,因为defer保存的变量都指向i,也就是同一个地址,当最后一个循环执行的时候,这个内存地址i的值,被置换成了3,所以defer里面所有的引用输出的就全是3了,这就是一个典型的闭包。正如上面的那句话总结的,闭包捕获的变量和常量是引用传递不是值传递
。
我们可以做个修改,把变量作为函数的参数传递给匿名函数,defer后面跟的就是一个函数调用了
for i := 0; i < 3; i++ {
defer func(item int) {
fmt.Println(item)
}(i)
}
我们变量i作为参数传递到函数中,我们知道go中函数的参数传递全是值传递,所以i就会被重新copy一份,输出指向的变量就不是i,而是每次被copy的新的地址空间。
当然我们也可以手动帮助它避免指向同一个地址空间
for i := 0; i < 3; i++ {
item := i
defer func() {
fmt.Println(item)
}()
}
输出
2
1
0
作为函数参数
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
打印下输出
2
1
0
defer后面跟函数参数,这样的输出就是正常了
defer命令执行的时机
在分析defer的执行时机之前,我们先看几段代码
example1
func f() (result int) {
defer func() {
result++
}()
return 0
}
example2
func f() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
example3
func f() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
我们先想想这个代码的输出,当然如果已经很清楚这个代码的输出,那么我想已经很明白defer的输出机制了。
我们来逐个分析逐个代码的输出
example1 的代码执行的过程是这样的
func f() (result int) {
result = 0 //return语句不是一条原子调用,return xxx其实是赋值+RET指令
func() { //defer被插入到return之前执行,也就是赋返回值和RET指令之间
result++
}()
return
}
所以上面的输出结果是1
再来分析example2,他可以被拆解为
func f() (r int) {
t := 5
r = t //赋值指令
func() { //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}()
return //空的return指令
}
所以他的输出是5
接下来分析example3,它的命令可以被拆解为
func f() (r int) {
r = 1 //给返回值赋值
func(r int) { //这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r + 5
}(r)
return //空的return
}
因为匿名函数r作为参数传进去了,go中函数之前参数的传递都是值传递,所以匿名函数里面的r是被重新复制了一份,指针的指向是新的地址空间。所以这个的输出是1。
那么我们可以来总结下defer的执行过程
1、返回值 = xxx
2、调用defer函数
3、空的return
所以我们看到defer的执行总是在return之前,并且总是先赋值,然后执行defer语句的。
defer配合recover
Panic
is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.
Recover
is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.
Panic
是一个内置函数,可停止常规控制流并开始恐慌。 当函数F调用恐慌时,F的执行停止,F中任何延迟的函数都将正常执行,然后F返回其调用方。 对于呼叫者,F然后表现得像是发生了恐慌。 该过程将继续执行堆栈,直到返回当前goroutine中的所有函数为止,此时程序崩溃。 紧急事件可以通过直接调用紧急事件来启动。 它们也可能是由运行时错误引起的,例如越界数组访问。
恢复
是一个内置函数,可以重新获得对紧急恐慌例程的控制。 恢复仅在延迟函数内部有用。 在正常执行期间,恢复调用将返回nil并且没有其他效果。 如果当前goroutine处于恐慌状态,则调用recover会捕获提供给panic的值并恢复正常执行。
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
打印下输出
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
我们发现,上面的函数有一个递归,知道函数在4的时候发生panic,这是通过defer挂起的recover就起作用了,它会帮助我们,从恐慌中恢复,从后往前恢复当前goroutine中所有的函数。
上面的demo,充分说明了defer配合recover的作用。能够帮助我们从异常中恢复。
不过在处理defer相关的时候,我们越早处理越好。因为panic在发生的时候,是从当前截止,向上去寻找defer定义的函数,然后一个个执行。但是如果,defer定义到了panic那么后面的将不会执行了。
总结
处理defer的时候我们越早越好,dafer要定义在panic之前,panic之后的defer是不能执行的,defer的执行是从下往上执行的,defer的执行总是在return之前,并且总是先赋值,然后执行defer语句的。对于defer调用匿名函数,我们需要变量的引用,避免发生闭包,出现莫名的错误。同时defer是存在性能问题的,对于性能要求高的我们还是要考虑放弃使用defer。
参考
【go语言并发编程实战】
【golang的defer精析】https://my.oschina.net/yuwenc/blog/300592
【深入理解 Go defer】https://segmentfault.com/a/1190000019303572
【go defer,panic,recover详解 go 的异常处理】https://www.jianshu.com/p/63e3d57f285f
【go defer,panic,recover-需要梯子】https://blog.golang.org/defer-panic-and-recover
【Golang之轻松化解defer的温柔陷阱】https://www.cnblogs.com/qcrao-2018/p/10367346.html#什么是defer
defer使用小结的更多相关文章
- Golang入门教程(十三)延迟函数defer详解
前言 大家都知道go语言的defer功能很强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦.Go 语言中延迟函数 defer 充当着 try...catch 的重任,使用起来也非常简便,然而在 ...
- Go语言中defer语句使用小结
defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源.关闭数据库连接.断开socket连接.解锁一个加锁的资源.Go语言机制担保一定会执行defer语句中的代 ...
- Promise与Defer认识
1.deffer对象:jquery的回掉函数解决方案:含义是延迟到未来某个点再执行: 2.$.ajax链式写法: $.ajax("test.php") .done(func ...
- 在Html中使用JavaScript的几点小结
前言 越发的意识到JS这门作为前端语言的重要性.所以下定决心这段时间在项目允许的情况下花大量时间在学习JS上.争取让自己的前端功底深厚一点. 小结 在包含外部js文件时,必须将src属性设置为指向相应 ...
- script标签加载顺序(defer & async)
script 拥有的属性 async:可选,表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本.只对外部脚本文件有效. charset:可选.表示通过 src 属性指 ...
- JQuery Ztree 树插件配置与应用小结
JQuery Ztree 树插件配置与应用小结 by:授客 QQ:1033553122 测试环境 Win7 jquery-3.2.1.min.js 下载地址: https://gitee.com/is ...
- Bootstrap 时间日历插件bootstrap-datetimepicker配置与应用小结
Bootstrap时间日历插件bootstrap-datetimepicker配置与应用小结 by:授客 QQ:1033553122 1. 测试环境 win7 JQuery-3.2.1.min ...
- Bootstrap Bootstrap表格插件bootstrap-table配置与应用小结
Bootstrap表格插件bootstrap-table配置与应用小结 by:授客 QQ:1033553122 1. 测试环境 win7 JQuery-3.2.1.min.js 下载地址: h ...
- 深入 Go 语言 defer 实现原理
转载请声明出处哦~,本篇文章发布于luozhiyun的博客: https://www.luozhiyun.com/archives/523 本文使用的go的源码 1.15.7 介绍 defer 执行规 ...
随机推荐
- java -输入年龄判断是否符合范围。
//创建的一个包名. package demo3; //定义一个类. public class Test { //公共静态的主方法. public static void main(String[] ...
- Android 开发技术周报 Issue#273
新闻/News Android 11有新玩法:双击手机背部截屏/进入多任务界面 Android 11 DP2证实了类似AirDrop的附近文件分享功能 谷歌发布Camera Go:即使入门机也能有出色 ...
- 深入分析mysql为什么不推荐使用uuid或者雪花id作为主键
前言:在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建 ...
- Loadrunner 11安装和破解
一.安装环境和文件准备 1.操作系统:Windows Server 2008 R2 Enterprise: 2.loadrunner版本:loadrunner 11: 3.安装浏览器:火狐39.0: ...
- SpringBoot(二) SpringBoot核心配置文件application.yml/properties
我们都知道在Spring中有着application.xml文件对Spring进行相关配置,通过web.xml中的contextConfigLocation指定application.xml文件所在位 ...
- java基础-对集合(Collection)的总结
集合(Collection)类型也是Java标准库中被使用最多的类型. List ArrayList LinkedList 对于自定义的对象,需要正确覆写 equals方法 参看7.3 例子 Map ...
- hdu4587 Two Nodes 求图中删除两个结点剩余的连通分量的数量
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4587 题目给了12000ms,对于tarjan这种O(|V|+|E|)复杂度的算法来说,暴力是能狗住的 ...
- [CVPR 2019]NOCS代码训练、检测与评估
Normalized Object Coordinate Space for Category-Level 6D Object Pose and Size Estimation 训练.检测与评估 本文 ...
- C语言学生管理系统
想练习一下链表,所以就有了这个用C写的学生管理系统 没有把它写入文件,才不是因为我懒哈哈哈,主要是为了练习链表的 #include<stdio.h> #include<stdlib. ...
- 拜托,别再问我什么是 B+ 树了
前言 每当我们执行某个 SQL 发现很慢时,都会下意识地反应是否加了索引,那么大家是否有想过加了索引为啥会使数据查找更快呢,索引的底层一般又是用什么结构存储的呢,相信大家看了标题已经有答案了,没错!B ...