go defer

参考解析

题目来源:腾讯

答案1:

使用defer的优势

defer一般用于资源的释放和异常的捕捉, 作为Go语言的特性之一.

defer 语句会将其后面跟随的语句进行延迟处理. 意思就是说 跟在defer后面的语言 将会在程序进行最后的return之后再执行.

在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

1.1 资源的释放
一般我们写读取文件的代码如下

  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. dst, err := os.Create(dstName)
  7. if err != nil {
  8. return
  9. }
  10. dst.Close()
  11. src.Close()
  12. return
  13. }

在程序最开始,os.Open及os.Create打开了两个文件资源描述符,并在最后通过file.Close方法得到释放,在正常情况下,该程序能正常运行,一旦在dstName文件创建过程中出现错误,程序就直接返回,src资源将得不到释放。因此需要在所有错误退出时释放资源,即修改为如下代码才能保证其在异常情况下的正确性。

  1. dst, err := os.Create(dstName)
  2. if err != nil {
  3. src.Close()
  4. }
  5. return

即在每个err里面如果发生了异常, 要及时关闭src的资源.
这个问题出现在加锁中也非常常见

  1. l.lock()
  2. // 如果下面发生了异常
  3. // 我们需要在每个err处理块中都加入l.unlock()来解锁
  4. // 不然资源就得不到释放, 就会产生死锁
  5. if err != nil {
  6. l.unlock()
  7. return
  8. }

但是这样做未免太麻烦了, defer优雅的帮我们解决了这个问题
比如我们可以这样

  1. src, err := os.Open(srcName)
  2. defer src.Close()
  3. if err != nil {
  4. return
  5. }
  6. dst, err := os.Create(dstName)
  7. defer dst.Close()
  8. if err != nil {
  9. return
  10. }
  11. ------------------------------------------
  12. l.lock()
  13. defer l.unlock()
  14. ......
  15. if err != nil {
  16. return
  17. }
  18. ......

这样写的话, 就不需要在每个异常处理块中都加上Close() 或者 unlock()语句了

defer 常用场景

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

defer经常和 panic 以及 recover 一起使用,判断是否有异常,进行收尾操作。

  1. package main
  2. import "fmt"
  3. func main(){
  4. defer func(){ // 必须要先声明defer,否则不能捕获到panic异常
  5. fmt.Println("c")
  6. if err:=recover();err!=nil{
  7. fmt.Println(err) // 这里的err其实就是panic传入的内容,55
  8. }
  9. fmt.Println("d")
  10. }()
  11. f()
  12. }
  13. func f(){
  14. fmt.Println("a")
  15. panic(55)
  16. fmt.Println("b")
  17. fmt.Println("f")
  18. }

输出结果:

  1. a
  2. c
  3. 55
  4. d

defer 规则

  1. 当申明defer 时,参数就已经解析了
  1. func a() {
  2. i := 0
  3. defer fmt.Println(i)
  4. i++
  5. return
  6. }
  7. //输出

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

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

这是因为虽然我们在defer后面定义的是一个带变量的函数: fmt.Println(i). 但这个变量(i)在defer被声明的时候,就已经确定其确定的值了

  1. defer执行顺序为先进后出

当同时定义了多个defer代码块时,golang安装先定义后执行的顺序依次调用defer。

  1. func b() {
  2. for i := 0; i < 4; i++ {
  3. defer fmt.Print(i)
  4. }
  5. }
  6. // 输出
  7. 3
  8. 2
  9. 1
  10. 0
  1. defer可以读取有名返回值
  1. func c() (i int) {
  2. defer func() { i++ }()
  3. return 1
  4. }

我们说过defer是在return调用之后才执行的。 这里需要明确的是defer代码块的作用域仍然在函数之内,结合上面的函数也就是说,defer的作用域仍然在c函数之内。因此defer仍然可以读取c函数内的变量