1. defer 的运作机制
defer 的运作离不开函数,这里至少有两点含义:
- 在 Go 中,只有在函数(和方法)内部才能使用 defer;
- defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。defer 将它们注册到其所在 goroutine 用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前被按后进先出(LIFO)的顺序调度执行(如下图所示)。

无论是执行到函数体尾部返回,还是在某个错误处理分支显式 return,亦或是出现 panic,已经存储到 deferred 函数栈中的函数都会被调度执行。因此,deferred 函数是一个可以在任何情况下都可以为函数进行收尾工作的好场合。
2. defer 的常见用法
1) 拦截 panic
可以如下面标准库代码中这样,重新 panic,但为新的 panic 传一个新的 error 值:
// $GOROOT/src/bytes/buffer.gofunc makeSlice(n int) []byte {// If the make fails, give a known error.defer func() {if recover() != nil {panic(ErrTooLarge)}}()return make([]byte, n)}
下面代码则是通过 deferred 函数拦截 panic 并恢复了程序的继续运行:
// deferred_func_3.gopackage mainimport "fmt"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")}$ go run deferred_func_3.goraise a panicrecovered from a panicmain exit normally
当 bizOperation 抛出 panic 时,函数 g 无法释放 mutex,而函数 f 则可以释放 mutex,让后续函数依旧可以申请 mutex 资源。
var mu sync.Mutexfunc f() {mu.Lock()defer mu.Unlock()bizOperation()}func g() {mu.Lock()bizOperation()mu.Unlock()}
虽然 deferred 函数可以拦截到绝大部分的 panic,但有些 runtime 之外的致命问题也是无法拦截并恢复的,比如下面代码中通过 C 代码”制造“的 crash,deferred 函数便无能为力:
// deferred_func_4.gopackage 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 函数拦截,但程序仍然崩溃掉了:
$ go run deferred_func_4.goSIGILL: illegal instructionPC=0x409a7f4 m=0 sigcode=1goroutine 0 [idle]:runtime: unknown pc 0x409a7f4... ...
2) deferred 函数可以修改函数的具名返回值
// $GOROOT/src/fmt/scan.gofunc (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.gofunc dialPlan9(ctx context.Context, net string, laddr, raddr Addr) (fd *netFD, err error) {defer func() { fixErr(err) }()... ...}
我们也来写一个更直观的示例:
// deferred_func_5.gopackage mainimport "fmt"func foo(a, b int) (x, y int) {defer func() {x = x * 5y = y * 10}()x = a + 5y = b + 6return}func main() {x, y := foo(1, 2)fmt.Println("x=", x, "y=", y)}
运行这个程序:
$ go run deferred_func_5.gox= 30 y= 80
3) deferred 函数可以用于输出一些调试信息
// $GOROOT/src/net/conf.gofunc (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 官方参考文档中提供的一个实现:
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()}
4) 还原变量旧值
defer 还有一种比较小众的用法,这用法依旧是来自对 Go 标准库源码的阅读。在 syscall 包下面有这样的一段代码:
// $GOROOT/src/syscall/fs_nacl.gofunc init() {// do not trigger loading of zipped file system hereoldFsinit := fsinitdefer func() { fsinit = oldFsinit }()fsinit = func() {}Mkdir("/dev", 0555)Mkdir("/tmp", 0777)mkdev("/dev/null", 0666, openNull)mkdev("/dev/random", 0444, openRandom)mkdev("/dev/urandom", 0444, openRandom)mkdev("/dev/zero", 0666, openZero)chdirEnv()}
3. 关于 defer 使用的几个关键问题
1) 明确哪些函数可以作为 deferred 函数
对于自定义的函数或方法,defer 可以给与无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃掉。
内置函数:
Functions:append cap close complex copy delete imag lenmake new panic print println real recover
append、cap、len、make、new 等内置函数是不能直接作为 deferred 函数的,而 close、copy、delete、print、recover 等是可以直接被 defer 注册为 deferred 函数的。
对于那些不能直接作为 deferred 函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求,以 append 为例:
defer func() {_ = append(sl, 11)}()
2) 把握好 defer 关键字后面表达式的求值时机
牢记一点:defer 关键字后面的表达式是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。
下面用一个典型的例子来说明一下 defer 后表达式的求值时机:
// deferred_func_7.gopackage mainimport "fmt"func foo1() {for i := 0; i <= 3; i++ {defer fmt.Println(i)}}func foo2() {for i := 0; i <= 3; i++ {defer func(n int) {fmt.Println(n)}(i)}}func foo3() {for i := 0; i <= 3; i++ {defer func() {fmt.Println(i)}()}}func main() {fmt.Println("foo1 result:")foo1()fmt.Println("\nfoo2 result:")foo2()fmt.Println("\nfoo3 result:")foo3()}
- foo1 中 defer 后面直接用的是 fmt.Println 函数,每当 defer 将 fmt.Println 注册到 deferred 函数栈的时候,都会对 Println 后面的参数进行求值,根据上述代码逻辑,依次压入 deferred 函数栈的函数是:
fmt.Println(0)fmt.Println(1)fmt.Println(2)fmt.Println(3)3210
- foo2 中 defer 后面接的是一个带有一个参数的匿名函数。每当 defer 将匿名函数注册到 deferred 函数栈的时候,都会对该匿名函数的参数进行求值,根据上述代码逻辑,依次压入 deferred 函数栈的函数是:
func(0)func(1)func(2)func(3)3210
- foo3 中 defer 后面接的是一个不带参数的匿名函数。根据上述代码逻辑,依次压入 deferred 函数栈的函数是:
func()func()func()func()
因此,当 foo3 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以 LIFO 次序出栈执行。匿名函数以闭包的方式访问外围函数的变量 i,并通过 Println 输出 i 的值,此时 i 的值为 4,因此 foo3 的输出结果为:
4444
鉴于 defer 表达式求值时机的重要性,我们再来看一个例子:
// deferred_func_8.gopackage mainimport "fmt"func foo1() {sl := []int{1, 2, 3}defer func(a []int) {fmt.Println(a)}(sl)sl = []int{3, 2, 1}_ = sl}func foo2() {sl := []int{1, 2, 3}defer func(p *[]int) {fmt.Println(*p)}(&sl)sl = []int{3, 2, 1}_ = sl}func main() {foo1()foo2()}
- foo1 中 defer 后面的匿名函数接收一个切片类型参数,当 defer 将该匿名函数注册到 deferred 函数栈的时候,会对它的参数进行求值,此时传入的变量 sl 的值为[]int{1, 2, 3},因此压入 deferred 函数栈的函数是:
func([]int{1,2,3})
之后虽然 sl 被重新赋值,但是当 foo1 返回后,deferred 函数被调度执行时,deferred 函数的参数值依然为[]int{1,2,3},因此 foo1 输出的结果为:[1 2 3]。
- foo2 中 defer 后面的匿名函数接收一个切片指针类型参数,当 defer 将该匿名函数注册到 deferred 函数栈的时候,会对它的参数进行求值,此时传入的参数为变量 sl 的地址,因此压入 deferred 函数栈的函数是:
func(&sl)
之后虽然 sl 被重新赋值。当 foo2 返回后,deferred 函数被调度执行时,deferred 函数的参数值依然为 sl 的地址,但此时 sl 的值已经变为[]int{3, 2, 1},因此 foo2 输出的结果为:[3 2 1]。
3) 知晓 defer 带来的性能损耗
我们用一个性能基准测试(benchmark)来直观地看看 defer 究竟带来多少性能损耗。
// defer_perf_benchmark_1_test.gopackage defer_testimport "testing"func sum(max int) int {total := 0for i := 0; i < max; i++ {total += i}return total}func fooWithDefer() {defer func() {sum(10)}()}func fooWithoutDefer() {sum(10)}func BenchmarkFooWithDefer(b *testing.B) {for i := 0; i < b.N; i++ {fooWithDefer()}}func BenchmarkFooWithoutDefer(b *testing.B) {for i := 0; i < b.N; i++ {fooWithoutDefer()}}
运行该 benchmark 测试,我们得到如下结果:
$ go test -bench . defer_perf_benchmark_1_test.gogoos: darwingoarch: amd64BenchmarkFooWithDefer-8 34581608 31.6 ns/opBenchmarkFooWithoutDefer-8 248793603 4.83 ns/opPASSok command-line-arguments 2.830s
我的测试:
$ go versiongo version go1.15.8 linux/amd64$ go test -bench . cli_test.gogoos: linuxgoarch: amd64BenchmarkFooWithDefer-8 153751154 7.65 ns/opBenchmarkFooWithoutDefer-8 238873606 5.02 ns/opPASSok command-line-arguments 3.677s
