defer的运作离不开函数,这至少有两层含义:
● 在Go中,只有在函数和方法内部才能使用defer;
● defer关键字后面只能接函数或方法,这些函数被称为deferred函数。defer将它们注册到其所在goroutine用于存放deferred函数的栈数据结构中,这些deferred函数将在执行defer的函数退出前被按后进先出(LIFO)的顺序调度执行,如下图:deferred函数的存储与调度执行
无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用return返回,抑或出现panic,已经存储到deferred函数栈中的函数都会被调度执行。
因此,deferred函数是一个在任何情况下都可以为函数进行收尾工作的好场合。我们回到本条开头的例子,把收尾工作挪到deferred函数中,变更后的代码如下:
func writeToFile(fname string, data []byte, mu *sync.Mutex) error {
mu.Lock()
defer mu.Unlock()
f, err := os.OpenFile(fname, os.O_RDWR, 0666)
if err != nil {
return err
}
defer f.Close()
_, err = f.Seek(0, 2)
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
return err
}
return f.Sync()
}
我们看到,defer的使用对函数writeToFile的实现逻辑的简化是显而易见的,资源释放函数的defer注册动作紧邻着资源申请成功的动作。这样成对出现的惯例极大降低了遗漏资源释放的可能性,开发人员再也不用小心翼翼地在每个错误处理分支中检查是否遗漏了某个资源的释放动作。同时,代码的简化又意味代码可读性的提高以及健壮性的增强。
defer的常见用法
除了释放资源这个最基本、最常见的用法之外,defer的运作机制决定了它还可以在其他一些场合发挥作用,这些用法在Go标准库中均有体现。
- 拦截panic
defer的运行机制决定了无论函数是执行到函数体末尾正常返回,还是在函数体中的某个错误处理分支显式调用return返回,抑或函数体内部出现panic,已经注册了的deferred函数都会被调度执行。
因此,defer的第二个重要用途就是拦截panic,并按需要对panic进行处理,可以尝试从panic中恢复(这也是Go语言中唯一的从panic中恢复的手段),也可以如下面标准库代码中这样触发一个新panic,但为新panic传一个新的error值:
package main
import "fmt"
var ErrTooLarge = 2
func makeSlice(n int) []byte {
// If the make fails, give a known error.
defer func() {
if recover() != nil {
fmt.Printf("333", 333)
panic(ErrTooLarge) // 触发一个新panic
}
}()
return make([]byte, n)
}
//下面的代码则通过deferred函数拦截panic并恢复了程序的运行:
// chapter4/sources/deferred_func_3.go
func bar() {
fmt.Println("raise a panic")
panic(-1)
}
func foo() {
defer func() {
if e := recover(); e != nil {
fmt.Println("recovered from a panic")
}
}()
bar()
}
func main() {
foo()
fmt.Println("main exit normally")
}
运行结果:
raise a panic
recovered from a panic
main exit normally
deferred函数在出现panic的情况下依旧能够被调度执行,这一特性让下面两个看似行为等价的函数在程序触发panic的时候得到不同的执行结果:
var mu sync.Mutex
func f() {
mu.Lock()
defer mu.Unlock()
bizOperation()
}
func g() {
mu.Lock()
bizOperation()
mu.Unlock()
}
当函数bizOperation抛出panic时,函数g无法释放mutex,而函数f则可以通过deferred函数释放mutex,让后续函数依旧可以申请mutex资源。
deferred函数虽然可以拦截绝大部分的panic,但无法拦截并恢复一些运行时之外的致命问题。比如下面代码中通过C代码“制造”的崩溃,deferred函数便无能为力:
package main
//#include <stdio.h>
//void crash() {
// int *q = NULL;
// (*q) = 15000;
// printf("%d\n", *q);
//}
import "C"
import (
"fmt"
)
func bar() {
C.crash()
}
func foo() {
defer func() {
if e := recover(); e != nil {
fmt.Println("recovered from a panic:", e)
}
}()
bar()
}
func main() {
foo()
fmt.Println("main exit normally")
}
执行这段代码我们就会看到,虽然有deferred函数拦截,但程序仍然崩溃了:
SIGILL: illegal instruction
PC=0x409a7f4 m=0 sigcode=1
goroutine 0 [idle]:
runtime: unknown pc 0x409a7f4
- 修改函数的具名返回值
下面是Go标准库中通过deferred函数访问函数具名返回值变量的两个例子:
func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
defer func() {
if e := recover(); e != nil {
if se, ok := e.(scanError); ok {
err = se.err
} else {
panic(e)
}
}
}()
...
}
// $GOROOT/SRC/net/ipsock_plan9.go
func dialPlan9(ctx context.Context, net string, laddr, raddr Addr) (fd *netFD, err error) {
defer func() { fixErr(err) }()
...
}
我们也来写一个更直观的示例:
package main
import "fmt"
func foo(a, b int) (x, y int) {
defer func() {
x = x * 5
println("3_x=", x)
y = y * 10
println("4_y=", y)
}()
x = a + 5
println("1_x=", x)
y = b + 6
println("2_y=", y)
return
}
func main() {
x, y := foo(1, 2)
fmt.Println("x=", x, "y=", y)
}
运行结果:
1_x= 6
2_y= 8
3_x= 30
4_y= 80
x= 30 y= 80
我们看到deferred函数在foo真正将执行权返回给main函数之前,将foo的两个返回值x和y分别放大了5倍和10倍。
输出调试信息
deferred函数被注册及调度执行的时间点使得它十分适合用来输出一些调试信息。比如,Go标准库中net包中的hostLookupOrder方法就使用deferred函数在特定日志级别下输出一些日志以便于程序调试和跟踪。
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
if c.dnsDebugLevel > 1 {
defer func() {
print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
}()
}
...
}
更为典型的莫过于在出入函数时打印留痕日志(一般在调试日志级别下),这里摘录Go官方参考文档中的一个实现:
package main
import "fmt"
func trace(s string) string {
fmt.Println("entering1:", s)
return s
}
func un(s string) {
fmt.Println("leaving4444:", s)
}
func a() {
defer un(trace("defer33-------------a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("22in b")
a()
}
func main() {
b()
}
运行结果如下:
entering1: b
22in b
entering1: defer33-------------a
in a
leaving4444: defer33-------------a
leaving4444: b