【一道面试题】你是如何理解defer的

155 阅读4分钟

一、defer简单介绍

1、含义

defer有延迟的意思,在Golang中是一种延迟处理机制,它可以确保在函数结束之前执行某些特定的操作。

2、基本用法

// defer + 执行语句
func t(){
    defer func(){
    }() 
    defer fmt.println("1")
    return 
}

当使用defer关键词后,后面的执行语句将会被注册,用于函数return之前执行。

二、常见的使用场景

基于defer的这种机制,在函数结束之前无论发生任何异常defer都会被执行,所以我们会使用它来实现资源管理异常捕获

1、资源管理

当函数打开一些文件句柄、socket操作时,如果中间发生错误,很有可能无法执行相应的close操作,从而导致资源连接无法关闭,导致内存泄漏问题。在执行并发操作时,我们会用的互斥锁来进行同步操作,当加锁后,数据处理完成不及时解锁将导致其他协程无法获得锁。使用defer后无论中间业务逻辑出现什么问题,在函数执行结束后,资源将会被正确回收。

//文件管理
func createFile(){
    file, err := os.Open()
    defer file.Close()
}

//socket管理
func createsocket(){
    socket , err := net.Listen("tcp",":8080")
    defer socket.Close()
}
//锁的释放
func op(){
    lock.Lock() 
    //todo 
    defer lock.Unlock()
}

2、异常捕获

panic是Go中的一个内建函数,用于触发一个运行时错误,当panic执行时开始逐级向上解包调用堆栈,直到被recover捕获,如果没有recover机制那么进程将直接崩溃。defer和recover配合将很好的捕获程序中的panic,防止进程运行时崩溃。

func main() {
   defer func() {
      if r := recover(); r != nil {
         fmt.Println("recovered in main", r)
      }
   }()
   ppanic()
}

func ppanic() {
   panic("there is a panic!")
}

3、日志记录

由于defer的机制,不管是触发了panic还是函数正常结束,我们都可以将它记录下来,这样我们可以准备的得知当前函数发生了哪些事情。

func someting(){
    defer func() {  
        if r := recover(); r != nil {
        //如果panic了就打印panic
                log.Printf("Frecovered from panic: %v", r)  
        } else {  
        //否则就记录一下事情
                log.Printf("someting") 
        }  
    }()  
}

4、多个defer的执行顺序

golang的defer是按照先进后出的方式来执行的,所以在代码中,最后注册(书写)的defer,将在最后执行。

package main

import "fmt"

func main() {
   defer f1()
   defer f2()
   defer f3()
}
func f1() {
   fmt.Println("this is f1")
}
func f2() {
   fmt.Println("this is f2")
}
func f3() {
   fmt.Println("this is f3")
}

//输出
/*
this is f3
this is f2
this is f1
*/

5、defer和return

我们都知道,在编程时return是函数的最后一步,而defer也是流程中最后一步执行,那么当defer遇到return时谁会最后执行呢。
首先我们先来了解一个概念,在Go中return并不是一个原子性操作,它分为赋值和返回两步操作,当return x或者某个变量时,首先会将x的值复制到return中的变量中,比如我们叫RET =X ,然后执行 return RET返回变量值。当与defer交互时,整个流程就变成了以下三步。

  1. set RET = X(某个变量)
  2. defer
  3. return RET

在Golang中函数的返回值有两种书写模式,分别是匿名和有名两种。我们分别按照上述的流程来看一下。

5.1 匿名模式

package main

import "fmt"

func main() {
   r := f1()
   fmt.Println("main value is ", r) //main value is  0
}
func f1() int {
   var i int
   defer func() {
      i++
      fmt.Println("value i is ", i) //value i is  1
   }()

   return i
}
//output 
value i is  1
main value is  0

按照上面的三步走流程,可以很清晰的解析这个输出。

  1. set RET = i, 此时i未被初始化赋值,所以此时的RET = 0
  2. 执行defer i++ , 输出line 14打印
  3. 执行return RET , line 8 打印结果 0
5.2 有名模式

package main

import "fmt"

func main() {
   r := f1()
   fmt.Println("main value is ", r) //main value is  1
}
func f1() (i int) {
   defer func() {
      i++
      fmt.Println("value i is ", i) //value i is  1
   }()

   return i
}

我们再来通过三步走流程解释一下

  1. set RET = i , 此时 i = 0
  2. 执行defer , line 13 打印 value is 1 , 并且此时i = 1 , RET = i= 1
  3. 执行return RET , line 8 打印 value is 1

三、使用defer的一些原则

1、不要使用defer来控制程序正常代码的流程

多个defer执行时有制定好的执行顺序,但是不要通过这个执行顺序来控制正常的代码。永远记住,defer主要是用来资源管理和异常管理的

2、同一函数内尽量不要使用多个defer

多个defer执行时,会有执行顺序,但这样也会使得代码难以理解和维护,这也是1中为什么这么建议,不要使用多余的defer,能合并的代码都在同一个defer中执行,降低代码的理解难度。

四、总结

这是Golang中经常会碰到的一道面试题,主要考察对于defer机制的了解程度,我们在平时使用中,对于recover和资源回收会更加了解和熟悉,通过这篇文章希望大家能够多多了解defer的其他应用场景以及defer和return之间复杂的关系。

我是烤鱼,后面会给大家带来更多面试题的解答,求点赞、收藏、关注。