Golang defer 使用时的坑

转载 2016年05月30日 18:11:16

defer是golang语言中的关键字,用于资源的释放,会在函数返回之前进行调用。一般采用如下模式:

f,err := os.Open(filename)
if err != nil {
    panic(err)
}
defer f.Close()

如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。

不过如果对defer的了解不够深入,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时。在讲解defer的实现之前先看一看使用defer容易遇到的问题。


先来看看几个例子。例1:

func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}

例2:

func f() (r int) {
     t := 5
     defer func() {
       t = t + 5
     }()
     return t
}

例3:

func f() (r int) {
    defer func(r int) {
          r = r + 5
    }(r)
    return 1
}

请读者先不要运行代码,在心里跑一遍结果,然后去验证。

例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6......

defer是在return之前执行的。这个在 官方文档中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!

函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。

其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:

返回值 = xxx
调用defer函数
空的return

先看例1,它可以改写成这样:

func f() (result int) {
     result = 0  //return语句不是一条原子调用,return xxx其实是赋值+ret指令
     func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
         result++
     }()
     return
}

所以这个返回值是1。

再看例2,它可以改写成这样:

func f() (r int) {
     t := 5
     r = t //赋值指令
     func() {        //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
         t = t + 5
     }
     return        //空的return指令
}

所以这个的结果是5。

最后看例3,它改写后变成:

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之间,因此可能有机会改变最终的返回值。

defer的实现

defer关键字的实现跟go关键字很类似,不同的是它调用的是runtime.deferproc而不是runtime.newproc。

在defer出现的地方,插入了指令call runtime.deferproc,然后在函数返回之前的地方,插入指令call runtime.deferreturn。

普通的函数返回时,汇编代码类似:

add xx SP
return

如果其中包含了defer语句,则汇编代码是:

call runtime.deferreturn,
add xx SP
return

goroutine的控制结构中,有一张表记录defer,调用runtime.deferproc时会将需要defer的表达式记录在表中,而在调用runtime.deferreturn的时候,则会依次从defer表中出栈并执行。

Go编程基础—defer、return、返回值之间执行顺序的坑

Golang中defer、return、返回值之间执行顺序的坑Go语言中延迟函数defer充当着 cry…catch 的重任,使用起来也非常简便,然而在实际应用中,很多gopher并没有真正搞明白de...
  • qq_22063697
  • qq_22063697
  • 2017年07月09日 16:37
  • 501

Golang有多个defer的情况

有多个defer时,return后先运行最后定义的defer,后进先出,栈的运行顺序 package main   import "fmt" import "log" fun...
  • muscledex
  • muscledex
  • 2017年05月04日 21:59
  • 325

Golang中defer、return、返回值之间执行顺序的坑

Go语言中延迟函数defer充当着 cry...catch 的重任,使用起来也非常简便,然而在实际应用中,很多gopher并没有真正搞明白defer、return和返回值之间的执行顺序,从而掉进坑中,...
  • txj236
  • txj236
  • 2016年06月23日 17:27
  • 2233

Golang(Go语言)读取文件基本用法

需要用到os和bufio包 import "os"import "bufio" 用os.Open打开一个文件,用bufio.NewReader来读取文件 file, er...
  • QQ245671051
  • QQ245671051
  • 2016年02月24日 21:34
  • 1567

理解Defer、Panic和Recover

刚开始的时候理解如何使用Defer和Recover有一点怪异,尤其是使用了try/catch块的时候。有一种模式可以在Go中实现和try/catch语句块一样的效果。不过之前你需要先领会Defer、P...
  • future_challenger
  • future_challenger
  • 2015年08月30日 15:53
  • 711

关于Go的闭包、匿名函数和defer的结合使用案例分析

Go里的“析构函数”defer关键字类似于PHP的__destruct(),用来标记最后执行的Go语句,一般用在资源释放、关闭连接等操作,会在函数关闭前调用。多个defer的定义与执行类似于栈的操作:...
  • qq_27682041
  • qq_27682041
  • 2017年12月13日 11:33
  • 107

golang:return和os.Exit有什么区别,return之后又执行了什么呢?

err := http.ListenAndServe(":"+svcPort, nil) if err != nil {      log.Println("http.ListenAndServe e...
  • freene
  • freene
  • 2018年01月03日 17:28
  • 136

Go实战--golang中defer的使用(有事没事defer一下)

生命不止,继续 go go go !!!学习golang这么久了,还没看到类似传统的 try…catch…finally 这种异常捕捉方式。 但是,Go中引入的Exception处理:defer, ...
  • wangshubo1989
  • wangshubo1989
  • 2017年07月04日 21:28
  • 2727

Go的异常处理 defer, panic, recover<转载>

Go语言追求简洁优雅,所以,Go语言不支持传统的 try…catch…finally 这种异常,因为Go语言的设计者们认为,将异常与控制结构混在一起会很容易使得代码变得混乱。因为开发者很容易滥用异常,...
  • wuwenxiang91322
  • wuwenxiang91322
  • 2013年06月06日 21:23
  • 33217

golang延迟函数defer

golang的defer优雅又简洁, 是golang的亮点之一。defer在声明时不会立即执行,而是在函数return后,再按照先进后出的原则依次执行每个defer,一般用于释放资源、清理数据、记录日...
  • yongche_shi
  • yongche_shi
  • 2016年11月16日 17:59
  • 813
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Golang defer 使用时的坑
举报原因:
原因补充:

(最多只允许输入30个字)