Go 语言里的 panic 状态位置问题

这里记录一下 Go 语言里 panic 的状态信息的变化和处理。用几个例子进行对比,后面是结论。

0x0 先看一个常见情况

func main() {
    defer func() {
        fmt.Printf("(a) %v\n", recover()) // (5)
    }()

    defer func() {
        fmt.Printf("(b) %v\n", recover()) // (4)
    }()

    defer func() {
        fmt.Printf("(c) %v\n", recover()) // (3)
    }()

    defer panic(1) // (2)
    panic(2)       // (1)

    fmt.Printf("unreachable here ..\n")

    panic(3)

    fmt.Printf("unreachable here ..\n")

    // Output:
    // (c) 1
    // (b) <nil>
    // (a) <nil>
}

注释里记录了输出的结果。这里的分析过程是这样的:

  • (1) 处产生的 panic,记录到当前函数(main)的状态里;
  • 按 LIFO 的顺序,依次执行 (2)、(3)、(4)、(5) 等处的 defer 调用;
  • (2) 处的调用产生 panic。这个 panic 发在当前 defer 函数里,如果能处理掉,那么不影响 (1) 处;但是,这里的 panic 在它自己所在的 defer 调用里没有处理掉,所以只能留到后面在 main 里处理。这样,就把 (1) 处产生的 panic 给覆盖掉了;
  • (3) 处的调用,会对 main 里的 panic 状态(上一步里已经被 (2) 处的 panic 覆盖了)进行处理,输出 (c) 1
  • (4) 处的调用,当前没有 panic 要处理,输出 nil;
  • (5) 同 (4)。

0x1 对比

修改了 (3) 处的代码,添加了 defer 的 recover() 处理:

修改变化:

        defer func() {
-               fmt.Printf("(c) %v\n", recover()) // (3)
+               defer func() {
+                       fmt.Printf("(c) %v\n", recover()) // (3)
+               }()
+               panic(3) // (3.1)
        }()

代码:

func main() {
    defer func() {
        fmt.Printf("(a) %v\n", recover()) // (5)
    }()

    defer func() {
        fmt.Printf("(b) %v\n", recover()) // (4)
    }()

    defer func() {
        defer func() {
            fmt.Printf("(c) %v\n", recover()) // (3)
        }()
        panic(3) // (3.1)
    }()

    defer panic(1) // (2)
    panic(2)       // (1)

    fmt.Printf("unreachable here ..\n")

    panic(3)

    fmt.Printf("unreachable here ..\n")

    // Output:
    // (c) 3
    // (b) 1
    // (a) <nil>
}

这里的分析过程是这样的:

  • (1) 处产生的 panic,记录到当前函数(main)的状态里;
  • 按 LIFO 的顺序,依次执行 (2)、(3.1)、(3)、(4)、(5) 等处的 defer 调用;
  • (2) 处的调用产生 panic。这个 panic 发在当前 defer 函数里,如果能处理掉,那么不影响 (1) 处;但是,这里的 panic 在它自己所在的 defer 调用里没有处理掉,所以只能留给后续在 main 里处理。这样,就把 (1) 处产生的 panic 给覆盖掉了;
  • (3.1) 处产生了 panic,记录到当前函数(defer)的状态里;但后续 (3) 处 defer 函数的 recover() 把这个 panic 处理掉了,同时输出 (c) 3
  • (4) 处的调用,当前 main 状态里的 panic 还在,所以 recover() 处理掉,并输出 (b) 1
  • (5) 处的调用,当前没有 panic 要处理,输出 nil;

0x2 再对比

0x1 的基础上,注释掉了 (3.1) 的 panic:

func main() {
    defer func() {
        fmt.Printf("(a) %v\n", recover()) // (5)
    }()

    defer func() {
        fmt.Printf("(b) %v\n", recover()) // (4)
    }()

    defer func() {
        defer func() {
            fmt.Printf("(c) %v\n", recover()) // (3)
        }()
        // panic(3) // (3.1)
    }()

    defer panic(1) // (2)
    panic(2)       // (1)

    fmt.Printf("unreachable here ..\n")

    panic(3)

    fmt.Printf("unreachable here ..\n")

    // Output:
    // (c) <nil>
    // (b) 1
    // (a) <nil>
}

(2) 产生的 panic,(3) 并没有看到,输出 (c) <nil>;在 (4) 位置才看到,并 recover 处理,输出 (b) 1

0x3 小结

以函数之间的调用关系来说明,比如这样的调用栈,其中 main() 是入口:

// (0)
// (1)
// (2)
// (3)                      /------- (5.1.1) defer
// (4)                     /
//      /----- (5.1) defer
//     /
// (5) <------ panic("555")
// (6)
// (7)
// (8)
// (9) main()

函数 (5) 产生的 panic("555"),会记录在 (5) 的状态里,如果 (5) 有 defer 函数、并且 recover 处理掉了,那么清空 panic 状态;否则就留到 (6) 的状态里。同样的,如果 (6) 没有处理,就留到 (7) 的状态里 ……,一直到被处理,或者程序崩溃。

如果 (5) 里的 defer 函数 (5.1) 产生了 panic,比如 panic("5.1"),因为 defer 函数有自己的状态,所以先记录在 (5.1) 的状态里,跟 (5) 里的 panic("555") 不冲突。如果 (5.1) 里有更深层的 defer 函数(比如 (5.1.1))的 recover 处理,那么清空 (5.1) 状态里的 panic;否则,就会把 (5.1) 里的 panic("5.1") 留到 (5) 的状态里,这样原先的 panic("555") 就被覆盖了。

另外,如果 (5.1) 没有产生 panic,(5.1.1) 里的 recover 能处理 (5) 里的 panic("555") 吗?结论是不能,因为看不到。

Read More: