首页 > 编程笔记 > Go语言笔记 阅读:7

Go语言defer函数的用法(非常详细)

我们知道,有一些资源(如数据库连接、文件句柄、锁等)是在函数中创建的,想要在函数执行完以后及时地释放这些资源,则需要手动关闭它们,但有时我们可能会忘记做这件事,所以 Go 语言提供了延时函数 defer。

defer 函数后面可以跟一句代码,也可以跟一个代码块(多条语句)。defer 函数内部的实现机制是先注册后调用,故而可以达到延迟执行的效果。

执行 defer 语句时,defer 的函数调用不是立即执行的,它的信息(比如要调用的函数和参数)会被存放到一个链表中。这个链表用于追踪同一协程中的所有 defer 调用。每个协程在运行时都有对应的结构体 g,结构体 g 有个字段指向 defer 链表头。多个 defer 链表连接起来就是一串 defer 结构体。每当有新的 defer 语句在协程中执行时,它的信息就会被添加到这个链表的头部。

这意味着,最后一个被添加的 defer 语句会是链表中的第一个 defer 语句。当其他普通函数(而不是整个协程)执行完毕开始处理其 defer 栈时,会从链表的头部开始执行,也就是最后存放的 defer 语句最先执行,这导致 defer 语句的执行顺序是倒序的,如下图所示。


图 1 defer链表倒序执行

Go defer函数的使用场景

下面来看一段关于使用 defer 函数的示例代码:
//测试defer函数的特性
func TestSimpleDefer(t *testing.T) {
        defer fmt.Println(1)
        defer fmt.Println(2)
        panic("产生panic")
        fmt.Println(3)
}
执行结果为:

2
1

panic: 产生panic
...

上述代码总共有三个输出语句,以下是相关说明:
正是因为 defer 函数具有延迟执行的特点,所以我们经常会在关闭数据库连接、关闭 I/O 连接或者释放锁的场景中使用 defer 函数。示例代码如下:
defer file.Close()

writer := bufio.NewWriter(file)
//bufio是往内存里面写数据的,所以在最后需要调用一次writer.Flush函数,以便将数据强制序列化到磁盘上
defer writer.Flush()
上述代码包含两个 defer 语句(因为具有延迟执行的特性,当前函数执行完以后在退出前才会执行 defer 语句):
因为这里使用了 bufio 写数据,而 bufio 又是先将数据写入内存中而不是直接写到磁盘上,所以直到调用 writer.Flush 函数才会强制将数据真正序列化到磁盘上。

当panic遇到defer函数

使用 defer 函数可以让代码更为健壮,它会减少由于忘记关闭资源而导致产生错误的情况,如果程序遇到 panic,会在退出前执行 defer 函数,示例代码如下:
func TestDeferWithPanic(t *testing.T) {
    //定义一个defer匿名函数,编写最后出栈时所做工作的相关代码
    defer func() {
            t.Log("Clear resources")
            ...
    }()
    t.Log("Started") //正常的逻辑
      //遇到了panic,仍然会执行defer函数
      //这样可以安全地将之前打开的类似输出、输入流的资源关闭
    panic("Fatal error")
}
编写代码时,通常会在创建资源的语句后紧跟 defer 函数相关的代码块,用以释放之前创建的资源。但是,协程并不会马上执行该代码块,只有在当前函数执行完或者遇到 panic 后,协程才会依次执行 defer 链表中的代码块。
func someFunction() {
   resource := acquireResource()

   defer func() {
       // 首先,检查并处理在函数执行过程中可能发生的 panic
       if r := recover(); r != nil {
          fmt.Println("Recovered from initial panic:", r)
       }

       // 然后,释放资源
       defer func() {
          if r := recover(); r != nil {
             fmt.Println("Recovered from panic during resource release:", r)
          }
       }()
       releaseResource(resource)
   }()

   // ... 其他代码 ...

   // 这里可能会触发一个 panic
   panic("something bad happened")
}

func acquireResource() *SomeResourceType {
    // 获取资源的逻辑
    return &SomeResourceType{}
}

func releaseResource(resource *SomeResourceType) {
   // 在释放资源时可能触发panic
   // panic("panic during resource release")
}
在上面的示例代码中,当 someFunction 函数中发生 panic 时,首先执行外层的 defer 函数相关代码块,用以释放之前创建的资源。该代码块可确保资源释放过程中的 panic 能被捕获和处理。

Go defer函数与for循环语句

建议避免在循环语句中使用 defer 函数,因为这与 defer 函数的设计理念不符。尽管 Go语言的开发者在不断优化 defer 函数的性能,如在 Go1. 13 版本中减少了在堆上的复制,在 Go1. 14 版本中通过展开代码来提升 defer 函数相关代码块的执行速度。但是,这些优化对于 for 循环中的 defer 函数却并不起作用。

使用 defer 函数时,defer 相关代码块会在包含该 defer 语句的函数执行完以后才执行,而不是在它被声明的代码块(如循环、条件语句等)执行完时执行。即使是在 for 循环内部,每个 defer 函数也会等外部函数执行完以后执行,而不是在每次循环结束时执行。这可能会在清理资源(例如关闭文件)时导致意想不到的结果。

因此,建议在 for 循环外部使用 defer 函数。让我们通过一个简单的例子来说明这一点。
func exampleFunction() {
   for i := 0; i < 3; i++ {
      defer fmt.Println("Deferred in loop:", i)
   }
   fmt.Println("Function executing")
}
 
func main() {
   exampleFunction()
   fmt.Println("Function finished")
}
在这个例子中,exampleFunction 包含一个循环,循环中有一个 defer 函数。尽管 defer 函数相关代码块在每次循环迭代时都会被执行,但它们是在 exampleFunction 函数执行完以后才会按照 defer 链表中 defer 语句存放的顺序倒序执行,即首先打印 "Function executing",然后打印 "Deferred in loop: 2",接着打印 "Deferred in loop: 1",之后打印 "Deferred in loop: 0",最后控制权回到 main 函数,打印 "Function finished"。

相关文章