你说你搞懂了defer?我不信!

2020/12/20 go

defer不仅可以用于资源释放,也可以用于流程控制和异常处理,但defer关键字只能用于函数或函数调用。

defer关键字后接一个匿名函数
defer func(){
    fmt.Println("hello world!")
}

defer关键字后接一个函数调用:
file,err :=os.open(name)
if err != nil {

   return nil,err
}
defer file.Close()

1、使用场景

1)释放资源
m.mutex.Lock()
defer m.mutex.Unlock()
defer 常用于文件句柄、数据库连接,停止定时器 Ticker 以及关闭管道等资源清理场景 。

2)流程控制
var wg wait.group
defer wg.Wait()
defer 也用于控制函数的执行顺序,比如配合wait.Group实现等待携程的退出。 

3)异常处理
defer 常用于处理异常,与recover配合可以消除panic,另外recover只能用于defer函数。

2、行为规则

1)规则一:延迟函数的参数在defer语句出现的时候就已经确定了
官方给出的例子如下图所示:
func f(){
    i :=0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定了,实际上是复制了一份。后面对变量i的修改
不会影响fmt.Println()函数的执行,仍然打印0。

2) 规则二:延迟函数按照先进后出的顺序执行,也就是说在函数中,最先出现的defer最后执行。


3)规则三:延迟函数可能影响主函数的返回值
定义defer的函数可能有名字的返回值,也可能是没名字的返回值!延迟函数可能会影响到返回值。

那如果想要理解延迟函数是如何影响主函数返回值的,只要明白函数是如何返回就够了。
(1)有个事实必须要清楚,关键字return不是一个原子操作,实际上return只代表汇编指令ret,即跳转程序的执行。
    比如return i,实际上是分两步执行,即先将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机介于返回值
    入栈与跳转之间。这意味着defer是有机会操作返回值的。
    
    那举个例子:
    func deferResult() (res int){
        i :=1
        
        defer func(){
            res++
        }
    
        return i    
    }
    该函数的return语句可以拆分成下面两行:
    result = i
    return
    前面说过延迟函数在return之前执行
    result = i //1
    result++ //2
    return
    
    所以上面的返回值是 result++ 的值
    
   (2)主函数有匿名函数返回值,则返回字面值。
    一个主函数拥有一个匿名返回值,比如说1,2,3,"hello"等,那么defer是无法操作返回值的
    比如下面一个返回字面值的函数:
    func f() int{
        var i int
        defer func(){
            i++
        }
        return 1
    }
    
    上面的return语句直接把1写入栈中作为返回值,延迟函数无法操作该返回值,索引就无法影响到该返回值。
    
   (3)主函数拥有匿名返回值,返回变量。
    一个主函数拥有一个匿名返回值,返回本地变量或者局部变量,这种情况下defer可以引用返回值,但不会改变返回值。
    一个返回本地变量的函数如下:
    func f() int{
      var i int
      defer func (){
        i++
      }
      return i
    
    }
    
    上面的函数返回一个局部变量,同时defer函数也会操作这个局部变量。对于一个匿名返回值来说,可以假定一个匿名变量
    来存储该返回值,那么上面的返回值可以拆分以下步骤:
    
    anony = i
    i++
    return
    
    由于i是整数,会将值复制给anony,所以defer语句中的修改不会函数返回值造成影响

Search

    Table of Contents