Go语言自习室之defer是个什么玩意儿

286 阅读5分钟

defer 是 Go 语言里一个非常有用的关键字,他能把资源的释放语句与申请语句放在距离相近的位置,从而减少了因遗忘释放而导致的资源泄露问题。当然,错误的使用defer也会造成一些问题甚至是故障。

延迟语句是什么

defer 是 Go 语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括 return 正常结束或者 panic 导致的异常结束)执行。在需要释放资源的场景非常有用。

defer 通常用于一些成对操作的场景:打开连接/关闭连接、加锁/释放锁、打开文件/关闭文件等。十分好用!

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

if f != nil{
    defer f.Close()
}

延迟语句的执行顺序

下面来读一段官方文档对 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 function 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.

jym各个是六级专八哥,这段话这里就不做翻译了。大致的意思就是说 defer 的执行,会将函数压栈,函数参数会被保存起来。当外层函数退出时,defer 函数按照定义的顺序逆序执行;但是如果 defer 执行的函数为空,那么将产生 panic。

这段定义中,可以找到很多关键的信息点:

  1. defer 执行顺序和栈的执行顺序相同,先进后出。
  2. defer 执行时会将函数参数保存起来。因此 defer 内参数的值与引用方式也有关系。

这时,先说明下。在 defer 函数定义时,对外部变量的引用有两种方式:函数参数、闭包引用。前者在 defer 定义时就把值传递给了 defer ,并被 cache 起来;而后者会在 defer 函数真正调用时根据整个上下文确定参数当前的值。

关于上文,defer 函数在执行时候,函数调用的参数会被保存起来,也就是复制了一份。真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是一个值,那么就和定义的时候是一致的。如果此变量是一个“引用”,那么就可能和定义时候不一致。,比如以下代码:

type number int

func (n number) print() {
   fmt.Println(n)
}
func (n *number) pprint() {
   fmt.Println(*n)
}

func main() {
   var n number
   defer n.print()
   defer n.pprint()
   defer func() { n.print() }()
   defer func() { n.pprint() }()
   n = 2
}

有以下输出

2
2
2
0

第一个结果 2 来源于第四个 defer 语句,它是闭包,对外部参数的引用,所以是 2 。第二个结果 2 来源于第三个 defer 语句,和第一个结果原因一致。第三个结果 2 来源于第二个 defer 语句,因为 pprint 函数的参数是地址的引用,所以是引用方式,最终求值为 2 。而第四个结果 0 来源于第一个 defer 语句。print 参数是简单的值传递,所以当执行到 defer n.print(),将 n 以 var n number 这句话后 n 的定义值保存起来,外部函数退出时再调用,因而输出的结果是 0 。

闭包是什么

闭包是函数及其相关引用环境组合而成的实体,即:闭包=函数+引用环境。

一般的函数都有函数名,而匿名函数没有。匿名函数不能独立存在,但可以直接调用或赋值于某个变量。匿名函数也被称为闭包,一个闭包继承了函数声明时的作用域。在 Go 语言中,所有的匿名函数都是闭包。

闭包在运行时可以有多个实例,它会将同一个作用域里的变量和常量捕获下来,无论是在什么地方被调用。而且,闭包捕获的变量和常量是引用传递,不是值传递。

延迟语句如何配合恢复语句

Go 语言中最让人诟病的不得不说说它的 error 了,实际项目中的 error 属于是百花齐放。不过有时候的问题处理,我们会选择直接 panic掉,如程序执行初始化的时候出问题,以此避免上线运行后出更大的问题。

而直接 panic之前,需要有一些“扫尾工作”,比如关闭客户端连接,防止客户端一直等待等等。panic 会停掉当前正在执行的程序,而不只是当前线程。在此之前,它会有序的执行完当前线程 defer 列表中的语句。其他协程中的 defer 语句不作保证。所以,在 defer 语句中定义一个 recover 语句,防止程序直接挂掉,就可以起到类似 JAVA 中的 try...catch 的效果。

注意:recover() 函数只在 defer 的函数中直接调用才有效。请看一下代码:

func main() {
   defer fmt.Println("defer main")
   var user = os.Getenv("USER_")

   go func() {
      defer func() {
         fmt.Println("defer call")
         if err := recover(); err != nil {
            fmt.Println("recover success err :", err)
         }
      }()

      func() {
         defer func() {
            fmt.Println("defer here")
         }()
         if user == "" {
            panic("should set user env")
         }
      }()
   }()

   time.Sleep(10)
   fmt.Println("end of main function")
}

执行结果如下:

defer here
defer call
recover success err : should set user env
end of main function
defer main

代码中的 panic 最终会被 recover 捕获到,这样的处理方式在一个 http server 的主流程中经常被用到,一个偶然的请求触发了某个 bug ,这时用 recover 捕获 panic ,稳住主流程,不影响其他请求。

最后来个小练习,考虑以下代码,思考程序能否正确 recover。

func main() {
   defer f()
   panic(404)
}

func f() {
   if err := recover(); err != nil {
      fmt.Println("recover err : ", err)
      return
   }
}

能。在 defer 的函数中调用,生效

func main() {
   recover()
   panic(404)
}

不能。直接调用 recover,返回nil

func main() {
   defer recover()
   panic(404)
}

不能。要在 defer 的函数中调用 recover。

func main() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println("recover err :", err)
      }
   }()
   panic(404)
}

能。在 defer 的函数中调用,生效。

func main() {
   defer func() {
      recover()
   }()
   panic(404)
}

能。在 defer 的函数中调用,生效。

func main() {
   defer func() {
      defer func() {
         recover()
      }()
   }()
   panic(404)
}

不能。多重 defer 嵌套。