想象这样一种场景:通常我们在编程的时候,经常会遇到资源申请完成后,再手工释放。比如打开一个文件后,再执行 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")
}
程序运行后输出:
图1 使用 defer 自动执行资源释放函数
这个例子非常简单,完全体现出了 defer 的功能。这里再解释一下 defer 执行时机。
- 正常情况下:一旦在某个函数或方法 F 中使用 defer 关键字注册了一个函数 f ,那么当函数 F 执行结束时,会自动调用 f.
- 一旦程序发生 panic 异常(异常位置当然必须得在 defer 关键字后面发生才有用),则会调用 f.
func F(param-list) return-list {
// ...
defer f(a, b, ...)
// ...
return x, y, ...
}
上面的例子中,你可以认为 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 main() {
process()
}
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:
test
f3
f2
f1
end
最后的结论就是:defer 函数的执行顺序和它的注册顺序是相反的,这有点像栈操作。最后一个被注册的函数会最先调用。
4. 总结
- 掌握 defer 用法
- 知道 defer 函数的执行时机