go语言的defer语句

Posted oxspirt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go语言的defer语句相关的知识,希望对你有一定的参考价值。

转: https://www.jianshu.com/p/5b0b36f398a2

------------------------------------------------------------

go语言defer语句的用法

defer的语法

defer后面必须是函数调用语句,不能是其他语句,否则编译器会出错。

package main

import "log"

func foo(n int) int 
    defer n++
    //defer log.Println("n=", n)
    return n


func main() 
    var i int = 100
    foo(i)

这个例子中defer后面使用的是n++指令,不是一个函数调用语句,编译器就报错:

# command-line-arguments
./main.go:6: expression in defer must be function call
./main.go:6: syntax error: unexpected ++ at end of statement

defer的基本功能

defer后面的函数在defer语句所在的函数执行结束的时候会被调用;我们查看一下汇编吗,看看defer是在什么时候被执行的:
定义两个函数foo1和foo2,功能和代码都是一样,只是其中一个包含defer语句,另一个没有。

func foo1(i int) int 
    i = 100
    i = 200
    return i


func foo2(i int) int 
    i = 100
    defer foo()
    i = 200
    return i

这是foo1的汇编代码:

func foo1(i int) int 
  44d660:   48 c7 44 24 10 00 00    movq   $0x0,0x10(%rsp)
  44d667:   00 00
    i = 100
  44d669:   48 c7 44 24 08 64 00    movq   $0x64,0x8(%rsp)
  44d670:   00 00
    i = 200
  44d672:   48 c7 44 24 08 c8 00    movq   $0xc8,0x8(%rsp)
  44d679:   00 00
    return i
  44d67b:   48 c7 44 24 10 c8 00    movq   $0xc8,0x10(%rsp)
  44d682:   00 00
  44d684:   c3                      retq
  ...

再看foo2的汇编代码:

func foo2(i int) int 
  44d690:   64 48 8b 0c 25 f8 ff    mov    %fs:0xfffffffffffffff8,%rcx
  44d697:   ff ff
  44d699:   48 3b 61 10             cmp    0x10(%rcx),%rsp
  44d69d:   76 70                   jbe    44d70f <main.foo2+0x7f>
  44d69f:   48 83 ec 18             sub    $0x18,%rsp
  44d6a3:   48 89 6c 24 10          mov    %rbp,0x10(%rsp)
  44d6a8:   48 8d 6c 24 10          lea    0x10(%rsp),%rbp
  44d6ad:   48 c7 44 24 28 00 00    movq   $0x0,0x28(%rsp)
  44d6b4:   00 00
    i = 100
  44d6b6:   48 c7 44 24 20 64 00    movq   $0x64,0x20(%rsp)
  44d6bd:   00 00
    defer foo()
  44d6bf:   c7 04 24 00 00 00 00    movl   $0x0,(%rsp)
  44d6c6:   48 8d 05 93 fb 01 00    lea    0x1fb93(%rip),%rax        # 46d260 <go.func.*+0x41>
  44d6cd:   48 89 44 24 08          mov    %rax,0x8(%rsp)
  44d6d2:   e8 e9 3e fd ff          callq  4215c0 <runtime.deferproc>
  44d6d7:   85 c0                   test   %eax,%eax
  44d6d9:   75 24                   jne    44d6ff <main.foo2+0x6f>
  44d6db:   eb 00                   jmp    44d6dd <main.foo2+0x4d>
    i = 200
  44d6dd:   48 c7 44 24 20 c8 00    movq   $0xc8,0x20(%rsp)
  44d6e4:   00 00
    return i
  44d6e6:   48 c7 44 24 28 c8 00    movq   $0xc8,0x28(%rsp)
  44d6ed:   00 00
  44d6ef:   90                      nop
  44d6f0:   e8 6b 48 fd ff          callq  421f60 <runtime.deferreturn>
  44d6f5:   48 8b 6c 24 10          mov    0x10(%rsp),%rbp
  44d6fa:   48 83 c4 18             add    $0x18,%rsp
  44d6fe:   c3                      retq
  ...

通过比较很容易看出foo2有两处需要注意,第一处是defer foo()语句的翻译,这个翻译我没有细看懂,我猜是准备foo的函数参数(如果有),然后保存这些参数值和foo的地址,注册到系统(runtime.deferproc);另一处是return指令的翻译,return指令的执行分三步,第一步拷贝return值到返回值内存地址,第二步会调用runtime.deferreturn去执行前面注册的defer函数,第三部再执行ret汇编指令。

有两个常见的defer语句应用场景是:

  • file对象打开后的自动关闭
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()

    // other codes
    return io.Copy(dst, src)

在打开输入文件输出文件后,不管后面的代码流程如何影响,这两个文件能够被自动关闭。

  • mutex对象锁住后的自动释放
func foo(...) 
    mu.Lock()
    defer mu.Unlock()

    // code logic

确保mu锁能够在函数foo退出之后自动释放。

注意0:如何让defer函数在宿主函数的执行中间执行

我们注意到defer函数的执行是在defer指令所在函数的运行结束之后,那么如何才能在所在函数的中间就释放呢,比如前面例子,在foo入口锁住了lock,而如果foo后半段的代码运行时间比较长,而此时又不需要继续保持住锁,该怎么办呢?

func foo() 
  mu.Lock();
  defer mu.Unlock();

  object, ok := map[key];
  if (!ok) 
      return
  
  // time-consuming operating with object
  ...

我们希望能在time-consuming operation 之前就释放锁,而不是等到整个foo返回。这有两个办法,一个是根据逻辑,把foo拆分两部分,前半部分需要锁,后半部分不需要锁;另一个办法是使用匿名函数:

package main

import "log"
import "time"
import "sync"

var mu sync.Mutex


func lock() 
    mu.Lock()
    log.Printf("lock")


func unlock() 
    mu.Unlock()
    log.Printf("unlock")


func foo() int 
    lock()

    func() 
        log.Printf("entry inner")
        defer unlock()
        log.Printf("exit inner")
    ()

    time.Sleep(1 * time.Second)
    log.Printf("return")
    return 0;


func main() 
    r := foo()
    log.Println("r=",r)

运行结果:

$ ./main 
2017/09/30 22:18:58 lock
2017/09/30 22:18:58 inner
2017/09/30 22:18:58 unlock
2017/09/30 22:18:59 return
2017/09/30 22:18:59 r= 0

从日志我们可以看出mu锁在sleep语句之前已经被释放了,而不是需要等到foo函数结束的时候才释放。

注意1:多个defer的执行顺序

如果函数里面有多条defer指令,他们的执行顺序是反序,即后定义的defer先执行。

package main

import "log"
import "time"

func foo(n int) int 
    defer log.Println("1111")
    time.Sleep(1 * time.Second)
    defer log.Println("2222")
    time.Sleep(1 * time.Second)
    defer log.Println("3333")
    time.Sleep(1 * time.Second)
    return n


func main() 
    var i int = 100
    foo(i)

运行结果如下,可以看出他们的调用顺序:

2017/09/30 19:22:03 3333
2017/09/30 19:22:03 2222
2017/09/30 19:22:03 1111

注意2:defer函数参数的计算时间点

defer函数的参数是在defer语句出现的位置做计算的,而不是在函数运行的时候做计算的,即所在函数结束的时候计算的。

package main

import "log"

func foo(n int) int 
    log.Println("n1=", n)
    defer log.Println("n=", n)
    n += 100
    log.Println("n2=", n)
    return n


func main() 
    var i int = 100
    foo(i)

其运行结果是:

2017/09/30 19:25:10 n1= 100
2017/09/30 19:25:10 n2= 200
2017/09/30 19:25:10 n= 100

可以看到defer函数的位置时n的值为100,尽管在函数foo结束的时候n的值已经是200了,但是defer语句本身所处的位置时刻,即foo函数入口时n为100,所以最终defer函数打印出来的n值为100。

注意3:如何在defer语句里面使用多条语句

前面我们提到defer后面只能是一条函数调用指令;而实际情况下经常会需要逻辑运行,会有分支,条件,而不是简单的一个log.Print指令;那怎么处理这种情况呢,我们可以把这些逻辑指令一起定义成一个函数,然后再调用这些函数就行了,命名函数或者匿名函数都可以,下面是一个匿名函数的例子:

package main

import "log"
import _ "time"

func foo(n int) int 
    log.Println("n1=", n)
    defer func() 
        n += 100
        log.Println("n=", n)
    ()
    n += 100
    log.Println("n2=", n)
    return n


func main() 
    var i int = 100
    foo(i)

运行结果:

2017/09/30 19:30:58 n1= 100
2017/09/30 19:30:58 n2= 200
2017/09/30 19:30:58 n= 300

眼尖的同学会发现其中的问题;为什么n打印出来是300呢,不是明明说好defer函数的参数值在它出现时候计算,而不是在运行的时候计算的吗,n应该打印出200才对啊?
同学,仔细看一下原文:defer函数的参数在defer语句出现的位置计算,不是在defer函数运行的时刻计算;人家明明说的很清楚,defer函数的参数,请问这里n是参数吗,不是哎,这里引用的是宿主函数的局部变量,而不是参数;所以它拿到的是运行时刻的值。

这就引发出下一个注意事项。

注意4:defer函数会影响宿主函数的返回值

package main

import "log"

func foo1(i *int) int 
    *i += 100
    defer func()  *i += 200 ()
    log.Printf("i=%d", *i)
    return *i


func foo2(i *int) (r int) 
    *i += 100
    defer func()  r += 200 ()
    log.Printf("i=%d", *i)
    return *i


func main() 
    var i, r int

    i,r = 0,0
    r = foo1(&i)
    log.Printf("i=%d, r=%d\n", i, r)

    i,r = 0,0
    r = foo2(&i)
    log.Printf("i=%d, r=%d\n", i, r)

运行结果为:

$ go build main.go && ./main 
2017/09/30 20:01:00 i=100
2017/09/30 20:01:00 i=300, r=100
2017/09/30 20:01:00 i=100
2017/09/30 20:01:00 i=100, r=300

这个例子其实有一点拗口的。
foo1 return指令前(i==100, ret==0),return指令后(i==100, ret=100),然后调用defer函数后(i==300,r==100),defer函数增加了i;main函数收到(i==300, r==100)
foo2 return指令前(i==100, ret==0),return指令后(i==100, ret=100),然后调用defer函数后(i==100,r==300),defer函数增加了ret;main函数收到(i==100, r==300)



作者:CodingCode
链接:https://www.jianshu.com/p/5b0b36f398a2
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

以上是关于go语言的defer语句的主要内容,如果未能解决你的问题,请参考以下文章

Go语言defer(延迟执行语句)

初识 go 语言:语法

Go语言自学系列 | golang defer语句

Go语言中defer的一些坑

Go语言开发Go语言错误处理

探究 Go 语言 defer 语句的三种机制