Simple error handling primitives:        https://github.com/pkg/errors

Defer, Panic, and Recover:      https://blog.golang.org/defer-panic-and-recover

golang的defer精析:     https://studygolang.com/articles/742

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中你算的是0,你就错了

如果example2中你觉得是10,你又错了...蒙对的不算...

如果example3中觉得得6,你又错了...如果你有算对的,也有算错,好吧...你丫的就是在蒙!

不懂的继续往下看啊.....

首先要明确的是:defer是在return之前执行的

这是官方文档中明确说明的http://golang.org/ref/spec#Defer_statements ,知道就行了,可以无视

然后要了解是的defer的实现方式,我以前有写过http://bbs.mygolang.com/thread-271-1-1.html

大意就是在defer出现的地方插入的指令

CALL runtime.deferproc

然后在函数返回之前的地方,插入指令

CALL runtime.deferreturn

再就是明确go返回值的方式跟C是不一样的,为了支持多值返回,go是用栈返回值的,而C是用寄存器。

最最最重要的一点就是:return xxx 这一句语句并不是一条原子指令!

整个return过程,没有defer之前是,先把在栈中写一个值,这个值被会当作返回值。然后再调用RET指令返回。return xxx语句汇编后是先给返回值赋值,再做一个空的return: ( 赋值指令 + RET指令)

defer的执行是被插入到return指令之前的

有了defer之后,就变成了 (赋值指令 + CALL defer指令 + RET指令)

而在CALL defer函数中,有可能将最终的返回值改写了...也有可能没改写。总之,如果改写了,那么看上去就像defer是在return xxx之后执行的~

这是所有你所想不明白的defer故事发生的根源。

上面的基础知识都有了,然后就可以来说说神奇的defer了。告诉大家一个简单的转换规则大家就再也不为defer迷糊了。

改写规则是将return语句分开成两句写,return xxx会被改写成:

返回值 = xxx

调用defer函数

空的return

先看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

}

所以这个例子结果是1

懂了么?

结论:defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return xxx语句并不是一条原子指令,defer被插入到了赋值 与 RET之前,因此可能有机会改变最终的返回值。

当你觉得迷糊时,可以用我给的这套规则转一下代码。

golang中defer的使用规则

在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用。因此,defer通常用来释放函数内部变量。

为了更好的学习defer的行为,我们首先来看下面一段代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
} dst, err := os.Create(dstName)
if err != nil {
return
} written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}

这段代码可以运行,但存在'安全隐患'。如果调用dst, err := os.Create(dstName)失败,则函数会执行return退出运行。但之前创建的src(文件句柄)没有被释放。 上面这段代码很简单,所以我们可以一眼看出存在文件未被释放的问题。 如果我们的逻辑复杂或者代码调用过多时,这样的错误未必会被及时发现。 而使用defer则可以避免这种情况的发生,下面是使用defer的代码:

func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close() dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close() return io.Copy(dst, src)
}

通过defer,我们可以在代码中优雅的关闭/清理代码中所使用的变量。defer作为golang清理变量的特性,有其独有且明确的行为。以下是defer三条使用规则。

规则一 当defer被声明时,其参数就会被实时解析

我们通过以下代码来解释这条规则:

func a() {
i := 0
defer fmt.Println(i)
i++
return
}

上面我们说过,defer函数会在return之后被调用。那么这段函数执行完之后,是不用应该输出1呢?

读者自行编译看一下,结果输出的是0. why?

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了。 换言之,上面的代码等同于下面的代码:

func a() {
i := 0
defer fmt.Println(0) //因为i=0,所以此时就明确告诉golang在程序退出时,执行输出0的操作
i++
return
}

为了更为明确的说明这个问题,我们继续定义一个defer:

func a() {
i := 0
defer fmt.Println(i) //输出0,因为i此时就是0
i++
defer fmt.Println(i) //输出1,因为i此时就是1
return
}

通过运行结果,可以看到defer输出的值,就是定义时的值。而不是defer真正执行时的变量值(很重要,搞不清楚的话就会产生于预期不一致的结果)

但为什么是先输出1,在输出0呢? 看下面的规则二。

规则二 defer执行顺序为先进后出

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。不要为什么,golang就是这么定义的。我们用下面的代码加深记忆和理解:

func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}

在循环中,依次定义了四个defer代码块。结合规则一,我们可以明确得知每个defer代码块应该输出什么值。 安装先进后出的原则,我们可以看到依次输出了3210.

规则三 defer可以读取有名返回值

先看下面的代码:

func c() (i int) {
defer func() { i++ }()
return 1
}

输出结果是12. 在开头的时候,我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量(如果无法读取函数内变量,那又如何进行变量清除呢....)。

当执行return 1 之后,i的值就是1. 此时此刻,defer代码块开始执行,对i进行自增操作。 因此输出2.

掌握了defer以上三条使用规则,那么当我们遇到defer代码块时,就可以明确得知defer的预期结果。

最新文章

  1. kindeditor本地上传报错,只限初学者
  2. iOS-工作经验+资料分享(长期更新)
  3. Tomcat部署方式
  4. dp - Codeforces Round #313 (Div. 1) C. Gerald and Giant Chess
  5. 01分数规划poj2728(最优比例生成树)
  6. 关于如果修改 ie 浏览器 文本模式
  7. shapefile文件
  8. C# Http请求(GET/HTTP/HTTPS)
  9. C# MySql 操作类
  10. sql 语句总结
  11. BZOJ 1083: [SCOI2005]繁忙的都市(MST)
  12. 运用bootstrap框架的时候 引入文件的问题
  13. KVO 进阶
  14. 201521123006 《Java程序设计》第3周学习总结
  15. ITU-T G.1080 IPTV的体验质量(QoE)要求 (Quality of experience requirements for IPTV services)
  16. 一起学Android之Dialog
  17. HTML5的快速使用和css3的入门知识汇总
  18. html table标签
  19. hadoop三种运行模式
  20. smb文件共享实现

热门文章

  1. Mac下Intellij IDea发布JavaWeb项目 详解一 (1、新建JavaEE Project并进行相应设置 2、配置tomcat)
  2. MyEclipse 10 下在线安装插件
  3. c++ 类内static成员初始化
  4. 查看iOS沙盒(SanBox)文件
  5. C语言程序设计--输入与输出
  6. 分布式搜索elasticsearch几个概念解析
  7. 【POJ2154】Color P&#243;lya定理+欧拉函数
  8. Swift - 判断应用是否是第一次启动(或当前版本是否第一次启动)
  9. binlog介绍
  10. Equinox P2 介绍(一)Getting Start