想象这样一种场景:通常我们在编程的时候,经常会遇到资源申请完成后,再手工释放。比如打开一个文件后,再执行 close 操作关闭文件描述符,如果你不关闭,则会产生描述符泄露。如果程序逻辑复杂,这非常容易出错。因为你的函数可能随时遇到错误而 return 出去,此时你极易忘记 close 掉申请过的资源。

Go 提供了关键字 defer 帮忙我们解决这个问题。它有点像 C++ 里的 RAII 机制,目的都是为了达到自动执行析构(资源释放)函数。

1. deferred 函数

不知道中文译名是什么,但是 defer 这个单词的意思是『推迟、延时』的意思。怎么使用它呢?先来看一个简单的例子:

package main

import "fmt"

// 模拟资源申请
func alloc() {
fmt.Println("allocate resource")
}

// 模拟资源释放
func free() {
fmt.Println("free resource")
}

// 测试函数
func test() {
alloc()
defer free()
fmt.Println("test alloc and free")
}

func main() {
test()
fmt.Println("end")
}

程序运行后输出:


047-Deferred 函数_资源释放


图1 使用 defer 自动执行资源释放函数


这个例子非常简单,完全体现出了 defer 的功能。这里再解释一下 defer 执行时机。

  • 正常情况下:一旦在某个函数或方法 F 中使用 defer 关键字注册了一个函数 f ,那么当函数 F 执行结束时,会自动调用 f.
  • 一旦程序发生 panic 异常(异常位置当然必须得在 defer 关键字后面发生才有用),则会调用 f.
func F(param-list) return-list {
// ...
defer f(a, b, ...)
// ...
return

上面的例子中,你可以认为 f 函数的执行时机是在 return 之后,最后一个花括号之前执行。接下来,我们通过几个示例,来看看 defer 还可以做哪些神奇的事情 。

2. defer 示例

2.1 记录函数运行时间

package main

import (
"fmt"
"time"
)

func process() {
defer trace("process")() // 注意 trace 函数会返回一个新函数,延时执行这个新函数
time.Sleep(5 * time.Second)
}

func trace(s string) func() {
start := time.Now()
fmt.Printf("%s start\n", s)
return func() {
fmt.Printf("%s end. elapse:%s\n", s, time.Since(start))
}
}

func

2.2 记录函数参数和返回值

package main

import (
"fmt"
)

// 配合有名返回值,defer 可以打印出返回值
func add(x, y int) (z int) {
defer func() {
fmt.Printf("input:%d,%d output:%d\n", x, y, z)
}()
return x + y
}

func main() {
add(1, 2)
}

3. defer 的执行顺序

如果一个函数中注册多个 defer 函数,那它们的执行顺序是怎样的呢?看下面的示例你就知道结果了。

package main

import "fmt"

func f1() {
fmt.Println("f1")
}
func f2() {
fmt.Println("f2")
}
func f3() {
fmt.Println("f3")
}

func test() {
defer f1()
defer f2()
defer f3()

fmt.Println("test")
}

func main() {
test()
fmt.Println("end")
}

上面的程序最终会输出:

// Output:

最后的结论就是:defer 函数的执行顺序和它的注册顺序是相反的,这有点像栈操作。最后一个被注册的函数会最先调用。

4. 总结

  • 掌握 defer 用法
  • 知道 defer 函数的执行时机