理解 Go defer, panic, recover

不像 Go 的其他流程控制(if, for, switch, goto, go) 一样, defer, panic 和 recover 用的比较少.

Defer

defer 语句会将函数调用放入一个列表. 当外围的函数执行返回后列表内保存的函数调用会被执行. defer 像 Python 的 with, finally 一样都具有类似资源情理的作用. 当然 defer 在这方面更灵活.

defer 的行为遵循 3 个简单的原则:

  • 一个 defer 函数的参数在放入待调用列表的时候被立即求值. 比如:

    func a() {
        i := 0
        defer fmt.Println(i) // @1
        i++
        return
    }
    

    这个例子中, @1 中的 i 会被立即求值, 所以 a 函数调用返回时, 会打印出 “0” 而不是 “1”.

  • defer 函数在外围函数返回时遵循 先进后出 的调用顺序.

    func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }
    }
    

    b 函数将会打印出 “3210”.

  • defer 函数会读取并且赋值外围函数的命名返回值(named return values).

    func c() (i int) {
        defer func() { i++ }()
        return 1
    }
    

    这个例子中 i 的值会 在 defer 被调用时执行加 1 操作, 所以 c 将返回 2. 这对于修改一个函数的 error 返回值很方便.

Panic

panic 是一个内置的函数用来阻断一般的控制流程. Go spec 对于 panic 有一段描述:

当执行函数 F 时, 一个明确的 panic 调用或者运行时 panic(数组越界访问等)会终止 F 的执行. F 所属的任何 defer 函数继续执行. 紧接着, 任何 F 调用者的 defer 函数运行, 而且对于执行中的 goroutine 中的任何顶层 defer 函数也是如此. 到那时, 程序终止并且报错(错误包含在 panic 参数中). 该终止序列被称为 panicking.

Recover

recover 是一个内建函数用来恢复一个 panicking 的 goroutine. Go spec 对于 recover 有一段描述:

设想有一个函数 G, 有一个 defer 函数会调用 recover 并且和 G 在同一 goroutine 的函数发生了 panic. 当运行到 defer 函数 D 调用的时候, D 调用 recover 的返回值是 panic 调用时传入的值. 如果 D 正常返回, 没有开启一个新的 panic, panicking 序列会终止. 在这种情况下, G 和 panic 之间的函数调用状态被废弃, 并且正常的调用恢复. 然后 G 中任何在 D 之前的 defer 函数会执行, 并且 G 的执行通过返回到它的调用者而终止.

以下情况中 recover 的返回值为 nil:

  • panic 参数为 nil;
  • 当前 goroutine 没有发生 panicking;
  • recover 没有被一个 defer 函数直接调用.

以下示例程序演示了 panic 和 defer 的原理:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

函数返回:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

如果我们移除 f 中 defer 将输出:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4

panic PC=0x2a9cd8
[stack trace omitted]

Go 标准库中的约定是, 即使一个包在内部使用 panic, 其外部 API 仍会给出明确的错误返回值(真实的例子可以参见标准库 json 包). 这是很好的使用原则, 尤其当我们写自己库的时候, 难免需要处理来自第三方的包返回的各种 error, 如果不需要处理, 简单的一个 panic 会是很好的方案.

结束

到这里可能就是你所能看到的关于 defer, panic 和 recover 的所有"坑"(我一向不觉得文档中有的你没看到的东西叫坑). 下面的内容可以不看了, 但是你看了, 保证没坏处.

当然可能有人说 recover 不能恢复所有的 panic, 比如如下的程序:

package main

import (
	"fmt"
	"sync"
)

func main() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("recovered")
		}
	}()

	m := map[int]int{}
	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		v := i
		go func() {
			defer wg.Done()
			m[v] = v
		}()
	}
	wg.Wait()
}

如果运行该程序, 那么会得到如下的输出:

fatal error: concurrent map writes
fatal error: concurrent map writes
...

其实这个不叫 panic, 按官方文档的说法叫: 崩溃(crash) 和 内存腐败(memory corruption). 由于程序无视 Go 内存模型 而导致的数据竞争(data race).

当然对于以上程序来说, 运行程序崩溃是显而易见的, 然而有的程序运行时不会显式的的崩溃, 如:

package main

import "fmt"

func main() {
	c := make(chan bool)
	m := make(map[string]string)
	go func() {
		m["1"] = "a"
		c <- true
	}()
	m["2"] = "b"
	<-c
	for k, v := range m {
		fmt.Println(k, v)
	}
}

输出:

2 b
1 a

为了检测和预防这种崩溃, Go 内建了数据竞争探测器(data race detector), 可以通过传递 flag: -race 给 go 命令来启用:

$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg

我们再次对上面的程序使用命令: go run -race main.go, 那么数据探测器可以检测到:

==================
WARNING: DATA RACE
Write at 0x00c420090180 by goroutine 6:
  runtime.mapassign_faststr()
      /usr/local/Cellar/go/1.10/libexec/src/runtime/hashmap_fast.go:694 +0x0
  main.main.func1()
      /Users/a/.go/src/github.com/gorocks/snippets/go/cmd/gosnippets/main.go:9 +0x5d

Previous write at 0x00c420090180 by main goroutine:
  runtime.mapassign_faststr()
      /usr/local/Cellar/go/1.10/libexec/src/runtime/hashmap_fast.go:694 +0x0
  main.main()
      /Users/a/.go/src/github.com/gorocks/snippets/go/cmd/gosnippets/main.go:12 +0xc9

Goroutine 6 (running) created at:
  main.main()
      /Users/a/.go/src/github.com/gorocks/snippets/go/cmd/gosnippets/main.go:8 +0x9a
==================
1 a
2 b
Found 1 data race(s)
exit status 66

参考资料

comments powered by Disqus