1. defer 的运作机制

defer 的运作离不开函数,这里至少有两点含义:

  • 在 Go 中,只有在函数(和方法)内部才能使用 defer;
  • defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数。defer 将它们注册到其所在 goroutine 用于存放 deferred 函数的栈数据结构中,这些 deferred 函数将在执行 defer 的函数退出前被按后进先出(LIFO)的顺序调度执行(如下图所示)。

image.png

无论是执行到函数体尾部返回,还是在某个错误处理分支显式 return,亦或是出现 panic,已经存储到 deferred 函数栈中的函数都会被调度执行。因此,deferred 函数是一个可以在任何情况下都可以为函数进行收尾工作的好场合。

2. defer 的常见用法

1) 拦截 panic

可以如下面标准库代码中这样,重新 panic,但为新的 panic 传一个新的 error 值:

  1. // $GOROOT/src/bytes/buffer.go
  2. func makeSlice(n int) []byte {
  3. // If the make fails, give a known error.
  4. defer func() {
  5. if recover() != nil {
  6. panic(ErrTooLarge)
  7. }
  8. }()
  9. return make([]byte, n)
  10. }

下面代码则是通过 deferred 函数拦截 panic 并恢复了程序的继续运行:

  1. // deferred_func_3.go
  2. package main
  3. import "fmt"
  4. func bar() {
  5. fmt.Println("raise a panic")
  6. panic(-1)
  7. }
  8. func foo() {
  9. defer func() {
  10. if e := recover(); e != nil {
  11. fmt.Println("recovered from a panic")
  12. }
  13. }()
  14. bar()
  15. }
  16. func main() {
  17. foo()
  18. fmt.Println("main exit normally")
  19. }
  20. $ go run deferred_func_3.go
  21. raise a panic
  22. recovered from a panic
  23. main exit normally

当 bizOperation 抛出 panic 时,函数 g 无法释放 mutex,而函数 f 则可以释放 mutex,让后续函数依旧可以申请 mutex 资源。

  1. var mu sync.Mutex
  2. func f() {
  3. mu.Lock()
  4. defer mu.Unlock()
  5. bizOperation()
  6. }
  7. func g() {
  8. mu.Lock()
  9. bizOperation()
  10. mu.Unlock()
  11. }

虽然 deferred 函数可以拦截到绝大部分的 panic,但有些 runtime 之外的致命问题也是无法拦截并恢复的,比如下面代码中通过 C 代码”制造“的 crash,deferred 函数便无能为力:

  1. // deferred_func_4.go
  2. package main
  3. //#include <stdio.h>
  4. // void crash() {
  5. // int *q = NULL;
  6. // (*q) = 15000;
  7. // printf("%d\n", *q);
  8. // }
  9. import "C"
  10. import (
  11. "fmt"
  12. )
  13. func bar() {
  14. C.crash()
  15. }
  16. func foo() {
  17. defer func() {
  18. if e := recover(); e != nil {
  19. fmt.Println("recovered from a panic:", e)
  20. }
  21. }()
  22. bar()
  23. }
  24. func main() {
  25. foo()
  26. fmt.Println("main exit normally")
  27. }

执行这段代码我们就会看到虽然有 deferred 函数拦截,但程序仍然崩溃掉了:

  1. $ go run deferred_func_4.go
  2. SIGILL: illegal instruction
  3. PC=0x409a7f4 m=0 sigcode=1
  4. goroutine 0 [idle]:
  5. runtime: unknown pc 0x409a7f4
  6. ... ...

2) deferred 函数可以修改函数的具名返回值

  1. // $GOROOT/src/fmt/scan.go
  2. func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
  3. defer func() {
  4. if e := recover(); e != nil {
  5. if se, ok := e.(scanError); ok {
  6. err = se.err
  7. } else {
  8. panic(e)
  9. }
  10. }
  11. }()
  12. ... ...
  13. }
  14. // $GOROOT/SRC/net/ipsock_plan9.go
  15. func dialPlan9(ctx context.Context, net string, laddr, raddr Addr) (fd *netFD, err error) {
  16. defer func() { fixErr(err) }()
  17. ... ...
  18. }

我们也来写一个更直观的示例:

  1. // deferred_func_5.go
  2. package main
  3. import "fmt"
  4. func foo(a, b int) (x, y int) {
  5. defer func() {
  6. x = x * 5
  7. y = y * 10
  8. }()
  9. x = a + 5
  10. y = b + 6
  11. return
  12. }
  13. func main() {
  14. x, y := foo(1, 2)
  15. fmt.Println("x=", x, "y=", y)
  16. }

运行这个程序:

  1. $ go run deferred_func_5.go
  2. x= 30 y= 80

3) deferred 函数可以用于输出一些调试信息

  1. // $GOROOT/src/net/conf.go
  2. func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
  3. if c.dnsDebugLevel > 1 {
  4. defer func() {
  5. print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
  6. }()
  7. }
  8. ... ...
  9. }

更为典型的莫过于在出入函数时打印留痕日志(一般在调试日志级别下),这里摘录一下 Go 官方参考文档中提供的一个实现:

  1. func trace(s string) string {
  2. fmt.Println("entering:", s)
  3. return s
  4. }
  5. func un(s string) {
  6. fmt.Println("leaving:", s)
  7. }
  8. func a() {
  9. defer un(trace("a"))
  10. fmt.Println("in a")
  11. }
  12. func b() {
  13. defer un(trace("b"))
  14. fmt.Println("in b")
  15. a()
  16. }
  17. func main() {
  18. b()
  19. }

4) 还原变量旧值

defer 还有一种比较小众的用法,这用法依旧是来自对 Go 标准库源码的阅读。在 syscall 包下面有这样的一段代码:

  1. // $GOROOT/src/syscall/fs_nacl.go
  2. func init() {
  3. // do not trigger loading of zipped file system here
  4. oldFsinit := fsinit
  5. defer func() { fsinit = oldFsinit }()
  6. fsinit = func() {}
  7. Mkdir("/dev", 0555)
  8. Mkdir("/tmp", 0777)
  9. mkdev("/dev/null", 0666, openNull)
  10. mkdev("/dev/random", 0444, openRandom)
  11. mkdev("/dev/urandom", 0444, openRandom)
  12. mkdev("/dev/zero", 0666, openZero)
  13. chdirEnv()
  14. }

3. 关于 defer 使用的几个关键问题

1) 明确哪些函数可以作为 deferred 函数

对于自定义的函数或方法,defer 可以给与无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃掉。

内置函数:

  1. Functions:
  2. append cap close complex copy delete imag len
  3. make new panic print println real recover

append、cap、len、make、new 等内置函数是不能直接作为 deferred 函数的,而 close、copy、delete、print、recover 等是可以直接被 defer 注册为 deferred 函数的。

对于那些不能直接作为 deferred 函数的内置函数,我们可以使用一个包裹它的匿名函数来间接满足要求,以 append 为例:

  1. defer func() {
  2. _ = append(sl, 11)
  3. }()

2) 把握好 defer 关键字后面表达式的求值时机

牢记一点:defer 关键字后面的表达式是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。

下面用一个典型的例子来说明一下 defer 后表达式的求值时机:

  1. // deferred_func_7.go
  2. package main
  3. import "fmt"
  4. func foo1() {
  5. for i := 0; i <= 3; i++ {
  6. defer fmt.Println(i)
  7. }
  8. }
  9. func foo2() {
  10. for i := 0; i <= 3; i++ {
  11. defer func(n int) {
  12. fmt.Println(n)
  13. }(i)
  14. }
  15. }
  16. func foo3() {
  17. for i := 0; i <= 3; i++ {
  18. defer func() {
  19. fmt.Println(i)
  20. }()
  21. }
  22. }
  23. func main() {
  24. fmt.Println("foo1 result:")
  25. foo1()
  26. fmt.Println("\nfoo2 result:")
  27. foo2()
  28. fmt.Println("\nfoo3 result:")
  29. foo3()
  30. }
  • foo1 中 defer 后面直接用的是 fmt.Println 函数,每当 defer 将 fmt.Println 注册到 deferred 函数栈的时候,都会对 Println 后面的参数进行求值,根据上述代码逻辑,依次压入 deferred 函数栈的函数是:
  1. fmt.Println(0)
  2. fmt.Println(1)
  3. fmt.Println(2)
  4. fmt.Println(3)
  5. 3
  6. 2
  7. 1
  8. 0
  • foo2 中 defer 后面接的是一个带有一个参数的匿名函数。每当 defer 将匿名函数注册到 deferred 函数栈的时候,都会对该匿名函数的参数进行求值,根据上述代码逻辑,依次压入 deferred 函数栈的函数是:
  1. func(0)
  2. func(1)
  3. func(2)
  4. func(3)
  5. 3
  6. 2
  7. 1
  8. 0
  • foo3 中 defer 后面接的是一个不带参数的匿名函数。根据上述代码逻辑,依次压入 deferred 函数栈的函数是:
  1. func()
  2. func()
  3. func()
  4. func()

因此,当 foo3 返回后,deferred 函数被调度执行时,上述压入栈的 deferred 函数将以 LIFO 次序出栈执行。匿名函数以闭包的方式访问外围函数的变量 i,并通过 Println 输出 i 的值,此时 i 的值为 4,因此 foo3 的输出结果为:

  1. 4
  2. 4
  3. 4
  4. 4

鉴于 defer 表达式求值时机的重要性,我们再来看一个例子:

  1. // deferred_func_8.go
  2. package main
  3. import "fmt"
  4. func foo1() {
  5. sl := []int{1, 2, 3}
  6. defer func(a []int) {
  7. fmt.Println(a)
  8. }(sl)
  9. sl = []int{3, 2, 1}
  10. _ = sl
  11. }
  12. func foo2() {
  13. sl := []int{1, 2, 3}
  14. defer func(p *[]int) {
  15. fmt.Println(*p)
  16. }(&sl)
  17. sl = []int{3, 2, 1}
  18. _ = sl
  19. }
  20. func main() {
  21. foo1()
  22. foo2()
  23. }
  • foo1 中 defer 后面的匿名函数接收一个切片类型参数,当 defer 将该匿名函数注册到 deferred 函数栈的时候,会对它的参数进行求值,此时传入的变量 sl 的值为[]int{1, 2, 3},因此压入 deferred 函数栈的函数是:
  1. func([]int{1,2,3})

之后虽然 sl 被重新赋值,但是当 foo1 返回后,deferred 函数被调度执行时,deferred 函数的参数值依然为[]int{1,2,3},因此 foo1 输出的结果为:[1 2 3]。

  • foo2 中 defer 后面的匿名函数接收一个切片指针类型参数,当 defer 将该匿名函数注册到 deferred 函数栈的时候,会对它的参数进行求值,此时传入的参数为变量 sl 的地址,因此压入 deferred 函数栈的函数是:
  1. func(&sl)

之后虽然 sl 被重新赋值。当 foo2 返回后,deferred 函数被调度执行时,deferred 函数的参数值依然为 sl 的地址,但此时 sl 的值已经变为[]int{3, 2, 1},因此 foo2 输出的结果为:[3 2 1]。

3) 知晓 defer 带来的性能损耗

我们用一个性能基准测试(benchmark)来直观地看看 defer 究竟带来多少性能损耗。

  1. // defer_perf_benchmark_1_test.go
  2. package defer_test
  3. import "testing"
  4. func sum(max int) int {
  5. total := 0
  6. for i := 0; i < max; i++ {
  7. total += i
  8. }
  9. return total
  10. }
  11. func fooWithDefer() {
  12. defer func() {
  13. sum(10)
  14. }()
  15. }
  16. func fooWithoutDefer() {
  17. sum(10)
  18. }
  19. func BenchmarkFooWithDefer(b *testing.B) {
  20. for i := 0; i < b.N; i++ {
  21. fooWithDefer()
  22. }
  23. }
  24. func BenchmarkFooWithoutDefer(b *testing.B) {
  25. for i := 0; i < b.N; i++ {
  26. fooWithoutDefer()
  27. }
  28. }

运行该 benchmark 测试,我们得到如下结果:

  1. $ go test -bench . defer_perf_benchmark_1_test.go
  2. goos: darwin
  3. goarch: amd64
  4. BenchmarkFooWithDefer-8 34581608 31.6 ns/op
  5. BenchmarkFooWithoutDefer-8 248793603 4.83 ns/op
  6. PASS
  7. ok command-line-arguments 2.830s

我的测试:

  1. $ go version
  2. go version go1.15.8 linux/amd64
  3. $ go test -bench . cli_test.go
  4. goos: linux
  5. goarch: amd64
  6. BenchmarkFooWithDefer-8 153751154 7.65 ns/op
  7. BenchmarkFooWithoutDefer-8 238873606 5.02 ns/op
  8. PASS
  9. ok command-line-arguments 3.677s