原则

  1. 不要相信任何外部输入的参数
    • 函数需要对所有输入的参数进行合法性的检查
    • 一旦发现问题,立即终止函数的执行,返回预设的错误值
  2. 不要忽略任何一个错误
    • 显式检查这些函数调用返回的错误值
    • 一旦发现错误,要及时终止函数执行,防止错误继续传播
  3. 不要假定异常不会发生
    • 异常不是错误
      • 错误是可预期的,也会经常发生,有对应的公开错误码和错误处理方案
      • 异常是不可预期的,通常指的是硬件异常、操作系统异常、语言运行时异常、代码 Bug(数组越界访问)等
    • 异常是小概率事件,但不能假定异常不会发生
      • 根据函数的角色和使用场景,考虑是否要在函数内设置异常捕获恢复的环节

panic

在 Go 中,由 panic 来表达异常的概念

  1. panic 指的是 Go 程序在运行时出现的一个异常情况
    • 如果异常出现了,但没有被捕获恢复,则 Go 程序的执行会被终止
    • 即便出现异常的位置不在主 goroutine
  2. panic 来源:Go 运行时 / 开发者通过 panic 函数主动触发
  3. 当 panic 被触发,后续的执行过程称为 panicking

手动调用 panic 函数,主动触发 panicking

  1. 当函数 F 调用 panic 函数,函数 F 的执行将停止
  2. 函数 F 中已进行求值的 deferred 函数都会得到正常执行
  3. 执行完所有 deferred 函数后,函数 F 才会把控制权返回给其调用者
  4. 函数 F 的调用者
    • 函数 F 之后的行为就如同调用者自己调用 panic 函数一样 - 递归
  5. panicking 过程将继续在栈上进行,直到当前 goroutine 中的所有函数都返回为止
  6. 最后 Go 程序崩溃退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

func foo() { // 同样没有捕获 panic
println("call foo")
bar() // bar 触发 panic 后,但没有捕获,会传递到 foo,就如同 foo 自身触发 panic,foo 函数会停止执行

println("exit foo")
}

func bar() { // 没有捕获 panic,panic 会沿着函数调用栈向上冒泡,直到被 recover() 捕获或者程序退出
println("call bar")
panic("panic in bar") // panicking 开始,函数执行到此后停止

zoo() // Unreachable code
println("exit bar") // Unreachable code
}

func zoo() {
println("call zoo")
println("exit zoo")
}

func main() {
println("call main")
foo()

println("exit main")
}

// Output:
// call main
// call foo
// call bar
// panic: panic in bar
//
// goroutine 1 [running]:
// main.bar(...)
// /Users/zhongmingmao/workspace/go/src/github.com/zhongmingmao/go101/main.go:11
// main.foo(...)
// /Users/zhongmingmao/workspace/go/src/github.com/zhongmingmao/go101/main.go:5
// main.main()
// /Users/zhongmingmao/workspace/go/src/github.com/zhongmingmao/go101/main.go:23 +0xa0

recover

recover 是 Go 内置的专门用于恢复 panic 的函数,必须被放在一个 defer 函数中才能生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main

import "fmt"

func foo() {
println("call foo")
bar()
println("exit foo")
}

func bar() {
defer func() { // 匿名函数
if err := recover(); err != nil {
// 如果 panic 被 recover 捕获,那么 panic 引发的 panicking 过程就会停止
fmt.Printf("recover in bar: %v\n", err)
}
}()

println("call bar")
panic("panic in bar") // 函数执行同样会被中断,先到已经成功设置的 defer 匿名函数

defer func() { // Unreachable code
println("will not be executed")
}()

zoo() // Unreachable code
println("exit bar") // Unreachable code
}

func zoo() {
println("call zoo")
println("exit zoo")
}

func main() {
println("call main")
foo()
println("exit main")
}

// Output:
// call main
// call foo
// call bar
// recover in bar: panic in bar
// exit foo
// exit main

defer 函数类似于 function close hook

每个函数都 defer + recover:心智负担 + 性能开销

经验

评估程序对 panic 的忍受度,对于关键系统,需要在特定位置捕获恢复 panic,以保证服务整体的健壮性

  1. Go http server,每个客户端连接都使用一个单独的 goroutine 进行处理的并发处理模型
  2. 客户端一旦与 http server 连接成功,http server 就会为这个连接新建一个 goroutine
    • 并在该 goroutine 中执行对应连接的 serve 方法,来处理这条连接上客户端请求
  3. panic 危害
    • 无论哪个 goroutine 中发生未被恢复的 panic,整个 Go 程序都将崩溃退出
  4. 需要保证某一客户端连接的 goroutine 出现 panic 时,不影响 http server 主 goroutine 的运行
    • serve 方法在一开始就设置了 defer 匿名函数,在 defer 匿名函数中捕获恢复了可能出现的 panic
  5. 并发程序的异常处理策略:局部不影响整体
net/http/server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()
...
}

提示潜在的 Bug - 触发了非预期的执行路径
在 Go 标准库中,大多数 panic 的使用都是充当类似断言的作用

encoding/json/decode.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// phasePanicMsg is used as a panic message when we end up with something that
// shouldn't happen. It can indicate a bug in the JSON decoder, or that
// something is editing the data slice while the decoder executes.
const phasePanicMsg = "JSON decoder out of sync - data changing underfoot?"

func (d *decodeState) init(data []byte) *decodeState {
d.data = data
d.off = 0
d.savedError = nil
d.errorContext.Struct = nil

// Reuse the allocated space for the FieldStack slice.
d.errorContext.FieldStack = d.errorContext.FieldStack[:0]
return d
}

func (d *decodeState) valueQuoted() interface{} {
switch d.opcode {
default:
// 如果程序执行流进入 default 分支,会触发 panic,提示开发人员,这可能是个 Bug
panic(phasePanicMsg)

case scanBeginArray, scanBeginObject:
d.skip()
d.scanNext()

case scanBeginLiteral:
v := d.literalInterface()
switch v.(type) {
case nil, string:
return v
}
}
return unquotedValue{}
}
encoding/json/encode.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (w *reflectWithString) resolve() error {
if w.v.Kind() == reflect.String {
w.s = w.v.String()
return nil
}
if tm, ok := w.v.Interface().(encoding.TextMarshaler); ok {
if w.v.Kind() == reflect.Ptr && w.v.IsNil() {
return nil
}
buf, err := tm.MarshalText()
w.s = string(buf)
return err
}
switch w.v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
w.s = strconv.FormatInt(w.v.Int(), 10)
return nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
w.s = strconv.FormatUint(w.v.Uint(), 10)
return nil
}
panic("unexpected map key type") // 相当于断言:代码不应该执行到这里,可能是个潜在的 Bug
}

不要混淆异常错误

  1. 在 Java 标准类库中的 checked exception,类似于 Go 的哨兵错误值
    • 都是预定义的,代表特定场景下的错误状态
  2. Java checked exception 用于一些可预见的,经常发生的错误场景
    • 针对 checked exception 所谓的异常处理,本质上是针对这些场景的错误处理预案
    • 即对 checked exception 的定义、使用、捕获等行为都是有意而为之
    • 必须要被上层代码处理:捕获 or 重新抛给上层
  3. 在 Go 中,通常会引入大量第三方包,而无法确定这些第三方 API 包中是否会引发 panic
    • API 的使用者不会逐一了解 API 是否会引发 panic,也没有义务去处理引发的 panic
    • 一旦 API 的作者将异常当成错误,但又不强制 API 使用者处理,会引入麻烦
    • 因此,不要将 panic 当成 error 返回给 API 的调用者,大部分应该返回 error,即 Java checked exception
Java Go
Checked Exception error
RuntimeException / Error panic

Java 发生 RuntimeException,JVM 只会停止对应的线程;而 Go 发生 panic,会整个程序崩溃