Go的 defer 语句用于预设一个函数调用(即 推迟执行 函数), 该函数会在执行 defer 的函数返回之前立即执行。
它显得非比寻常, 但却是处理一些事情的有效方式,例如无论以何种路径返回,都必须释放资源的函数。 典型的例子就是解锁互斥和关闭文件。
// Contents 将文件的内容作为字符串返回。
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close 会在我们结束后运行。
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append 将在后面讨论。
if err != nil {
if err == io.EOF {
break
}
return "", err // 我们在这里返回后,f 就会被关闭。
}
}
return string(result), nil // 我们在这里返回后,f 就会被关闭。
}
推迟诸如 Close 之类的函数调用有两点好处:第一, 它能确保你不会忘记关闭文件。如果你以后又为该函数添加了新的返回路径时, 这种情况往往就会发生。第二,它意味着“关闭”离“打开”很近, 这总比将它放在函数结尾处要清晰明了。
被推迟函数的实参(如果该函数为方法则还包括接收者)在 推迟 执行时就会求值, 而不是在 调用 执行时才求值。
这样不仅无需担心变量值在函数执行时被改变, 同时还意味着单个已推迟的调用可推迟多个函数的执行。下面是个简单的例子。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
被推迟的函数按照后进先出(LIFO)的顺序执行,因此以上代码在函数返回时会打印 4 3 2 1 0。
一个更具实际意义的例子是通过一种简单的方法, 用程序来跟踪函数的执行。我们可以编写一对简单的跟踪例程:
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
// 像这样使用它们:
func a() {
trace("a")
defer untrace("a")
// 做一些事情....
}
我们可以充分利用这个特点,即被推迟函数的实参在 defer 执行时才会被求值。 跟踪例程可针对反跟踪例程设置实参。以下例子:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
会打印:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
对于习惯其它语言中块级资源管理的程序员,defer 似乎有点怪异, 但它最有趣而强大的应用恰恰来自于其基于函数而非块的特点。在 panic 和 recover 这两节中,我们将看到关于它可能性的其它例子。
参考文章:
- Effective Go: https://golang.org/doc/effective_go.html#defer