GO语言defer用法总结

目录

1. defer关键字

2. defer的使用场景

2.1 资源的释放

2.2 配合recover一起处理panic

3. defer与return


1. defer关键字

defer顾名思义,延迟。它是go语言中的一个关键字,主要用在函数或方法前面,作用是用于函数和方法的延迟调用,在语法上,defer与普通的函数调用没有什么区别。 在使用上非常简单,只需要弄清楚以下几点即可:

  1. 延迟的函数的什么时候被调用?

  2. 函数return的时候

  3. 发生panic的时候

  4. 延迟调用的语法规则

  5. defer关键字后面表达式必须是函数或者方法调用

  6. 延迟内容不能被括号括起来

defer执行顺序
上一小节说到defer关键字后面的函数调用会在函数return或者发生panic的时候执行,这个在单个defer的时候很好理解,但当一个函数中有多个defer的时候,他们的顺序是怎么样的呢?defer语句的执行顺序是先进后出LIFO。 下面看个具体例子:

package main

import "fmt"

func defer1() {
        fmt.Println("defer1")
}

func defer2() {
        fmt.Println("defer2")
}

func defer3() {
        fmt.Println("defer3")
}

func main() {
        defer defer1()
        defer defer2()
        defer defer3()
}

运行结果:

defer3
defer2
defer1

可以看到执行顺序跟栈是一样的,先调用,后执行

2. defer的使用场景

通过前面的小节我们知道了defer关键字主要是用于延迟调用,那么什么场景下需要我们用到延迟调用了,有过golang基础的同学在一些代码中经常看到defer关键字。defer关键字一般用在以下两个场景中

2.1 资源的释放

通过defer延迟调用机制,我们可以简洁优雅处理资源回收问题,从而避免在复杂的代码逻辑情况下,遗漏相关的资源回收问题,用的比较多的就是类似网络连接,数据库连接,以及文件句柄的资源的释放。 看看下面一个复制文件的函数,

func CopyFile(dstFile, srcFile string) (wr int64, err error) { 
    src, err := os.Open(srcFile)     
    if err != nil {         
        return     
    }      
    dst, err := os.Create(dstFile)     
    if err != nil {         
        return    
    }      
    wr, err = io.Copy(dst, src)     
    dst.Close()     
    src.Close()     
    return 
}

仔细看这段代码,其实是有问题的,比如当地6行执行失败,程序直接返回了,但我们并没有关闭前面打开的文件资源src,这样就造成了资源的浪费。 那么用defer我们要怎么做呢?

func CopyFile(dstFile, srcFile string) (wr int64, err error) {
    src, err := os.Open(srcFile)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstFile)
    if err != nil {
        return
    }
    defer dst.Close()
    
    wr, err = io.Copy(dst, src)  
    return wr, err
}

只要我们正确打开了某个资源,比如srcdst,没返回err的情况下,都可以用defer延迟调用来关闭资源,注意,这是go语言中非常常见的一种资源关闭方式。

2.2 配合recover一起处理panic

defer另一个常用的地方就是在处理程序panic的时候,关于程序的异常捕获我们将在下一个小节讲到,这里大家可以先了解一下,go语言中用panic来抛出异常,用recover来捕获异常,所以当我们的程序出现异常的时候,我们需要知道是发生了什么异常的时候,就可以用defer recover来捕获异常。

package main

import "fmt"

func main() {
   defer func() {
      if r := recover(); r != nil {
         fmt.Println(r)
      }
   }()
   a := 1
   b := 0
   fmt.Println("result:", a/b)
}

运行结果:

runtime error: integer divide by zero

可以看到,程序并没有输出result,这是因为我们尝试对一个除数为0的数做除法,这是不允许的,所以程序回panic,但我们用defer在程序发生panic的时候捕获了这个异常,打印出异常信息:runtime error: integer divide by zero

3. defer与return

前面第一小节我们介绍过defer函数的执行是在return的时候,那么在具体一点,在return的时候,defer具体做了什么?又会带来什么结果?这是一个非常值得探讨的问题,也是面试官在面试中经常会问的问题,往往通过这个问题就可一看出一个面试者对go语言掌握的扎不扎实。
例子1

package main

import "fmt"


func deferRun() {
  var num = 1
  defer fmt.Printf("num is %d", num)
  
  num = 2
  return
}

func main(){
    deferRun()
}

运行结果:

num is 1

为什么? 延迟函数 defer fmt.Printf("num is %d", num) 的参数numdefer语句出现的时候就已经确定,num=1,所以不管后面怎么修改a的值,最终调用defer函数传递给defer函数的参数已经固定是1了,不会再变化
例子2:

package main

import "fmt"

func main() {
 deferRun()
}

func deferRun() {
 var arr = [4]int{1, 2, 3, 4}
 defer printArr(&arr)
 
 arr[0] = 100
 return
}

func printArr(arr *[4]int) {
 for i := range arr {
  fmt.Println(arr[i])
 }
}

运行结果:

100
2
3
4

为什么? 通过前一个地址,我们知道在defer出现的时候,参数已经确定,但是这里传递的是地址,地址没变,但是地址对应的内容被修改了,所以输出会被修改
例子3:

package main

import "fmt"

func main() {
   res := deferRun()
   fmt.Println(res)
}

func deferRun() (res int) {
  num := 1
  
  defer func() {
    res++
  }()
  
  return num
}

运行结果:

2

为什么? 这是一个非常经典的例子,要想准确的的只程序的执行结果,需要我们对函数return的执行有一个细致的了解。其实函数的return并非一个原子操作,return的过程可以被分解为以下三步:

  1. 设置返回值

  2. 执行defer语句

  3. 将结果返回

所以,在本例中,第一步是将result的值设置为num,此时还未执行defernum的值是1,所以result被设置为1,然后再执行defer语句将result+1,最终将result返回,所以会打印出2
例子4:

package main

import "fmt"

func main() {
    res := deferRun()
    fmt.Println(res)
}

func deferRun() int {
  var num int
  defer func() {
    num++
  }()
  
  return 1
}

运行结果:

1

本例和前面的区别返回值是匿名的,但是我们可以同样运用上面的思路,自己创建一个返回值,这里假设为res,运用前面的思路分析,第一步将res设置为1,第二步执行defernum+1,第三步将res返回,所以最终结果是1
例子5:

package main

import "fmt"

func main() {
    res := deferRun()
    fmt.Println(res)
}

func deferRun() int {
  num := 1
  defer func() {
    num++
  }()
  
  return num
}

运行结果:

1

同样的思路不难分析:自己创建一个返回值,这里假设为res,第一步将res设置为num,所以res的值为1,第二步执行defernum+1,此时num为2,但是res为1,第三步将res返回,所以最终结果是1
例子6:

package main

import "fmt"

func main() {
    res := deferRun()
    fmt.Println(res)
}

func deferRun() (res int) {
  num := 1
  defer func() {
    num++
  }()
  
  return num
}

运行结果:

1

不难分析运行结果还是1,同样的三步分析法,因为defer改变的是num的值,而不是改变的res的值,所以结果不会变,不过defer函数里变为res++,那么结果就是2了。 所以,当我们碰到deferreturn确定最终的返回值,可以总结为以下两点:

  1. defer 定义的延迟函数的参数在 defer 语句出时就已经确定下来了

  2. return 不是原子级操作,执行过程是: 设置返回值—>执行 defer 语句—>将结果返回


    关注我,观看体系化的技术系列文章。持续给大家分享,不仅仅有后端技术,还有一些行业新鲜事儿,更有一些职场心得以及求职心得。一起聊聊互联网那些事儿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值