一、panic概述

异常

异常在一些其他语言中,如c++、java,被叫做Exception,主要由抛出异常捕获异常两部分组成。

异常在go语言中,叫做panic,且由panicrecover方法组成,panic用来抛出,recover用来从panic中恢复。

示例

以下是一段简单的panic和recover使用示例:

 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
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) {
    fmt.Println("Printing in g", i)
    panic(i)
    fmt.Println("After panic in g", i)
}

我们先把defer recover部分注释,运行结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Calling g.
Printing in g 0
panic: 0

goroutine 1 [running]:
main.g(0x4b14a0)
	/tmp/sandbox2444947193/prog.go:18 +0x94
main.f()
	/tmp/sandbox2444947193/prog.go:12 +0x5d
main.main()
	/tmp/sandbox2444947193/prog.go:6 +0x19

Program exited.

可以看到程序运行到g方法的第二行时,产生的panic导致进程异常退出,后续的代码都没有执行。

再把recover注释部分打开,运行结果为:

1
2
3
4
5
6
Calling g.
Printing in g 0
Recovered in f 0
Returned normally from f.

Program exited.

f方法中的recover捕获了panic,打印了panic传递的参数,并且main方法是正常返回的。g方法panic之后的代码没有执行。

官方翻译

panic是go的内置函数,它可以终止程序的正常执行流程并发出panic。比如当函数F调用panic,F的执行将被终止,并返回到调用者。对调用者而言,F就像调用者直接调用了panic。该过程一直跟随堆栈向上,直到当前goroutine中的所有函数都返回,此时程序崩溃。panic可以通过直接调用panic产生。同时也可能由运行时的错误所产生,例如数组越界访问。

recover是go语言的内置函数,它的唯一作用是可以从panic中重新控制goroutine的执行。recover必须通过defer来运行。在正常的执行流程中,调用recover将会返回nil且没有什么其他的影响。但是如果当前的goroutine产生了panic,recover将会捕获到panic抛出的信息,同时恢复其正常的执行流程

小结

  • panic可以令程序崩溃(异常退出)
  • recover可以让程序从panic中恢复,并正常运行
  • 即使单个goroutine中发生了panic,也会使整个进程崩溃
  • recover必须通过defer来运行

二、panic从哪儿来

我们可以手动调用内置函数panic,但是那些空指针、数组越界等运行时panic是如何被检测到的,下面针对这一问题做一些代码调试

常见的几种panic

  1. 空指针 invalid memory address or nil pointer dereference
  2. 数组越界 index out of rangeslice bounds out of range
  3. 除数为零 integer divide by zero
  4. 自定义panic

追踪panic来源

测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main
func main() {
    a := 0
    testDivide(a) //除零
    //testOutRange() //越界
    //testNil() //空指针
    //panic("666") //自定义panic
}
func testDivide(a int) {
    b := 10 / a
    _ = b
}
func testOutRange() {
    var a []int
    a[0] = 2
}
func testNil() {
    var a *int
    *a = 1
}

调试代码

与linux平台下的gdb调试工具类似,dlv用来调试go语言编写的程序。

dlv是一个命令行工具,它包含了多个调试命令,例如运行程序、下断点、打印变量、step in、step out等。我们常用的go语言编辑器,如vscode、golang等的可视化调试也是调用dlv。

找出panic是怎么产生的:

这里我们先给出结论,具体调试过程产生的代码,请往下看

  1. 调试自定义panic方法:

    1. 在8行处下断点
    2. 打印main方法的汇编代码
    3. 可以看到panic方法编译后实质是runtime包中的gopanic方法
  2. 使用dlv调试testDivide中的代码,有以下几个关键步骤:

    1. 在12行处下断点
    2. 打印testDivide方法的汇编代码
    3. testDivide方法中测试参数a的值是否为零
    4. 如果为零,则调用runtime包的panicdivide方法
    5. 调用runtime包的panicdivide方法
    6. panicdivide方法调用了panic
    7. 打印panicdivide的汇编代码,panic方法编译后实质是runtime包中的gopanic方法
  • panic方法实际调用了runtime.gopanic
  • 编译后的testDivide方法中除了正常的除法逻辑,编译器塞入了判断除数是否为零的代码分支,当除数为零则进入panic流程,与自定义panic相同,同样调用了runtime.gopanic
  • 其他数组越界及空指针,也都是调用了runtime.gopanic进入panic流程,不同的是:数组越界与除数为零相似,是通过编译器塞入判断分支进行越界检测;而空指针是通过访问非法地址产生中断进入panic流程。

小结

  • panic可以由开发者调用内置函数抛出
  • 编译器将检测异常的代码加入到程序中,会出现异常时抛出
  • 某些非法指令产生中断,并由中断处理函数抛出

以下是调试全过程(不喜跳过,看下一节): testDivide方法调试:

 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
PS E:\xxx\liuwei\test> .\dlv.exe debug main.go
Type 'help' for list of commands.
(dlv) b main.go:12 //下断点
Breakpoint 1 set at 0x45b25e for main.testDivide() E:/xxx/liuwei/test/main.go:12
(dlv) c //运行
> main.testDivide() E:/xxx/liuwei/test/main.go:12 (hits goroutine(1):1 total:1) (PC: 0x45b25e)
     7:         //testNil()
     8:         //panic("666")
     9: }
    10:
    11: func testDivide(a int) {
=>  12:         b := 10 / a
    13:         _ = b
    14: }
    15:
    16: func testOutRange() {
    17:         var a []int
(dlv) disass //打印汇编代码
TEXT main.testDivide(SB) E:/xxx/liuwei/test/main.go
        main.go:11      0x45b250        4883ec10        sub rsp, 0x10
        main.go:11      0x45b254        48896c2408      mov qword ptr [rsp+0x8], rbp
        main.go:11      0x45b259        488d6c2408      lea rbp, ptr [rsp+0x8]
=>      main.go:12      0x45b25e*       488b4c2418      mov rcx, qword ptr [rsp+0x18] //向寄存器rcx中写入参数a的值
        main.go:12      0x45b263        4885c9          test rcx, rcx //测试rcx中的值是否为0
        main.go:12      0x45b266        7502            jnz 0x45b26a //如果不为0,跳转到0x45b26a,执行正常的除逻辑
        main.go:12      0x45b268        eb25            jmp 0x45b28f //否则跳转到0x45b28f 
        main.go:12      0x45b26a        b80a000000      mov eax, 0xa //地址0x45b26a,以下为正常的除逻辑
        main.go:12      0x45b26f        4883f9ff        cmp rcx, -0x1
        main.go:12      0x45b273        7407            jz 0x45b27c
        main.go:12      0x45b275        4899            cqo
        main.go:12      0x45b277        48f7f9          idiv rcx
        main.go:12      0x45b27a        eb05            jmp 0x45b281
        main.go:12      0x45b27c        48f7d8          neg rax
        main.go:12      0x45b27f        31d2            xor edx, edx
        main.go:12      0x45b281        48890424        mov qword ptr [rsp], rax
        main.go:14      0x45b285        488b6c2408      mov rbp, qword ptr [rsp+0x8]
        main.go:14      0x45b28a        4883c410        add rsp, 0x10
        main.go:14      0x45b28e        c3              ret
        main.go:12      0x45b28f        e8fceffcff      call $runtime.panicdivide //地址0x45b28f ,调用runtime包的panicdivide方法
        main.go:1       0x45b294        90              nop
 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
(dlv) si //单条指令执行
> main.testDivide() E:/xxx/liuwei/test/main.go:1 (PC: 0x45b258)
=>   1: package main
        2:
        3: func main() {
        4:         testDivide()
        5:         //testOutRange()
        6:         //testNil()
(dlv) disass //打印汇编代码
TEXT main.testDivide(SB) E:/xxxliuwei/test/main.go
           main.go:9       0x45b240        4883ec10                sub rsp, 0x10
           main.go:9       0x45b244        48896c2408              mov qword ptr [rsp+0x8], rbp
           main.go:9       0x45b249        488d6c2408              lea rbp, ptr [rsp+0x8]
           main.go:10      0x45b24e        48c7042400000000        mov qword ptr [rsp], 0x0
           main.go:11      0x45b256*       eb00                    jmp 0x45b258
=>      main.go:1       0x45b258        e833f0fcff              call $runtime.panicdivide //由于除数为0,跳转到了此处,准备panic
           main.go:1       0x45b25d        90                      nop
(dlv) si //单条指令执行
> runtime.panicdivide() C:/go1.13/go/src/runtime/panic.go:176 (PC: 0x42a290)
Warning: debugging optimized function
      171:         panic(shiftError)
      172: }
      173:
      174: var divideError = error(errorString("integer divide by zero"))
      175:
=> 176: func panicdivide() { //进入runtime包的panicdivide方法
      177:         panicCheck2("integer divide by zero")
      178:         panic(divideError)
      179: }
      180:
      181: var overflowError = error(errorString("integer overflow"))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(dlv) disass //打印汇编代码
TEXT runtime.panicdivide(SB) C:/go1.13/go/src/runtime/panic.go
=>      panic.go:176    0x42a290        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]
           panic.go:176    0x42a299        488b8900000000          mov rcx, qword ptr [rcx]
           panic.go:176    0x42a2a0        483b6110                cmp rsp, qword ptr [rcx+0x10]
           panic.go:176    0x42a2a4        764d                    jbe 0x42a2f3
           panic.go:176    0x42a2a6        4883ec18                sub rsp, 0x18
           panic.go:176    0x42a2aa        48896c2410              mov qword ptr [rsp+0x10], rbp
           panic.go:176    0x42a2af        488d6c2410              lea rbp, ptr [rsp+0x10]
           panic.go:177    0x42a2b4        488d058e370500          lea rax, ptr [_image_base__+514633]
           panic.go:177    0x42a2bb        48890424                mov qword ptr [rsp], rax
           panic.go:177    0x42a2bf        48c744240816000000      mov qword ptr [rsp+0x8], 0x16
           panic.go:177    0x42a2c8        e8c3f7ffff              call $runtime.panicCheck2
           panic.go:178    0x42a2cd        488b056cba0900          mov rax, qword ptr [runtime.divideError]
           panic.go:178    0x42a2d4        488b0d6dba0900          mov rcx, qword ptr [runtime.divideError+8]
           panic.go:178    0x42a2db        4885c0                  test rax, rax
           panic.go:178    0x42a2de        7404                    jz 0x42a2e4
           panic.go:178    0x42a2e0        488b4008                mov rax, qword ptr [rax+0x8]
           panic.go:178    0x42a2e4        48890424                mov qword ptr [rsp], rax
           panic.go:178    0x42a2e8        48894c2408              mov qword ptr [rsp+0x8], rcx
           panic.go:178    0x42a2ed        e8be0b0000              call $runtime.gopanic //panic编译以后是runtime包中的gopanic方法
           panic.go:178    0x42a2f2        90                      nop
           panic.go:176    0x42a2f3        e8c8620200              call $runtime.morestack_noctxt
           panic.go:176    0x42a2f8        eb96                    jmp $runtime.panicdivide

自定义panic调试:

 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
PS E:\xxx\liuwei\test> .\dlv.exe debug main.go
Type 'help' for list of commands.
(dlv) b main.go:7 //下断点
Breakpoint 1 set at 0x45b224 for main.main() E:/xxx/liuwei/test/main.go:7
(dlv) c //运行
> main.main() E:/xxx/liuwei/test/main.go:7 (hits goroutine(1):1 total:1) (PC: 0x45b224)
        2:
        3: func main() {
        4:         //testDivide()
        5:         //testOutRange()
        6:         //testNil()
=>   7:         panic("666")
        8: }
        9:
       10: func testDivide() {
       11:         a := 0
       12:         a = 10 / a
(dlv) disass //打印汇编代码
TEXT main.main(SB) E:/xxx/liuwei/test/main.go
           main.go:3       0x45b200        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]
           main.go:3       0x45b209        488b8900000000          mov rcx, qword ptr [rcx]
           main.go:3       0x45b210        483b6110                cmp rsp, qword ptr [rcx+0x10]
           main.go:3       0x45b214        762b                    jbe 0x45b241
           main.go:3       0x45b216        4883ec18                sub rsp, 0x18
           main.go:3       0x45b21a        48896c2410              mov qword ptr [rsp+0x10], rbp
           main.go:3       0x45b21f        488d6c2410              lea rbp, ptr [rsp+0x10]
=>      main.go:7       0x45b224*       488d0555c70000          lea rax, ptr [_image_base__+424320]
           main.go:7       0x45b22b        48890424                mov qword ptr [rsp], rax
           main.go:7       0x45b22f        488d05ca870200          lea rax, ptr [_image_base__+539136]
           main.go:7       0x45b236        4889442408              mov qword ptr [rsp+0x8], rax
           main.go:7       0x45b23b        e870fcfcff              call $runtime.gopanic //panic编译以后是runtime包中的gopanic方法
           main.go:7       0x45b240        90                      nop
           main.go:3       0x45b241        e87a53ffff              call $runtime.morestack_noctxt
           main.go:1       0x45b246        ebb8                    jmp $main.main

三、panic到哪儿去

panic后的处理流程

由于panic和defer有着难解难分的关系,我们先了解一下defer。

defer定义的官翻:

defer语句将函数调用保存到一个列表上。保存的调用列表在当前函数返回前执行。Defer通常用于简化执行各种清理操作的函数。

通俗地说,就是defer保证函数调用不管在什么情况下(即使当前函数发生panic),在当前函数返回前必然执行。另外defer的函数调用符合先进后出的规则,即先defer的函数后执行。

我们看一个示例程序,它是第一节示例程序的升级版本,方法g中会调用自身:

 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
package main

import "fmt"

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

func f() {
	/*defer func() {
	    if r := recover(); r != nil {
	        fmt.Println("Recovered in f", r)
	    }
	}()*/
	defer func() {
		fmt.Println("defer in f")
	}()
	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)
}

程序运行结果如下:

 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
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
defer in f
defer in main
panic: 4

goroutine 1 [running]:
main.g(0x4)
        /tmp/sandbox2114608904/prog.go:30 +0x1ec
main.g(0x3)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x2)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x1)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x0)
        /tmp/sandbox2114608904/prog.go:34 +0x136
main.f()
        /tmp/sandbox2114608904/prog.go:23 +0x7f
main.main()
        /tmp/sandbox2114608904/prog.go:9 +0x3f

Program exited

从运行结果可以观察到defer的作用,即使方法g中当i为4时发生了panic,每个defer的函数调用依然正常被执行了,而且是先进后出的顺序被执行。就像是每次defer时,将被defer的函数调用push到一个栈数据结构中,当返回时,再从栈中挨个将defer的函数pop出来并执行。

recover函数调用必须使用defer关键字,就是因为defer的函数调用必然会被执行。可以将以上实例中defer recover部分打开观察输出,与第一节中defer recover输出类似,程序可以正常执行并正常退出。

源码分析

我们再对源码做一下简单分析,以加深对panic及recover处理流程的理解。

首先简单了解下有关defer的一对方法:deferproc和deferreturn。

  • deferproc即defer关键字的实现,它将defer的函数调用push到当前goroutine中的defer链表头部
  • deferreturn,当一个函数中包含defer操作,编译器将在函数返回前插入一条deferreturn调用,deferreturn会将当前函数中defer的函数调用依次执行完毕

panic方法对应的实现为runtime.gopanic,recover方法对应的实现为runtime.gorecover。

源码如下(为了简化理解,省略了很多分支判断,只保留主流程的代码):

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
func gopanic(e interface{}) {
    //获取当前goroutine的对象gp
    gp := getg()
    ...
    //将当前panic添加到gp的panic链表头部
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    ...
    //循环执行defer链表中的函数
    for {
        //获取gp的defer链表
        d := gp._defer
        if d == nil {
            //如果没有defer,退出循环
            break
        }
        ...
        done := true
        ...
        //执行defer的函数调用
        var regs abi.RegArgs
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz), uint32(d.siz), &regs)
        ...
        p.argp = nil
        d._panic = nil
        ...
        if done {
            //清理defer对象,并设置下一个defer对象到gp的defer链表头部
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
        }
        if p.recovered {
            //如果defer运行了recover函数,调用内置的recovery函数恢复调用
            //recovery函数会将当前的调用栈改变到deferreturn,从而使得程序可以继续正常运行
            ...
            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    //如果没有recover,defer执行完毕,打印panic信息,并退出进程
    preprintpanics(gp._panic)
    fatalpanic(gp._panic) // should not return
    *(*int)(nil) = 0      // not reached
}

//recover方法的实现
func gorecover(argp uintptr) interface{} {
    
    gp := getg()
    p := gp._panic
    ...
    //recover方法仅有的一个作用,将recovered置为true
    p.recovered = true
    return p.arg
}

小结

  • panic处理过程中会检测是否有defer的函数调用
  • 如果有,按照先进后出的顺序依次执行
  • 如果defer中有recover调用,则将调用栈修改到deferreturn,使得程序正常执行
  • 否则当defer的函数调用执行完后,打印panic信息,进程退出

最后

最后我们通过一个简单的例子,看一下recover后如何打印panic信息,及如何阅读panic信息

示例是一个除零的panic:

  1. recover后,调用printPanicInfo方法
  2. printPanicInfo使用runtime.Stack方法收集调用堆栈信息
  3. r为recover返回的参数,即panic传入的参数,一般为panic的具体原因,本示例为:runtime error: integer divide by zero
  4. 将panic原因和堆栈信息拼接并打印
 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
package main
import (
    "fmt"
    "runtime"
)
func main() {
    f()
}
func f() {
    defer func() {
        if r := recover(); r != nil {
            printPanicInfo(r)
        }
    }()
    g()
}
func g() {
    a := 10
    var b int
    a = a / b
}
func printPanicInfo(r interface{}) {
    buf := make([]byte, 64<<10)
    buf = buf[:runtime.Stack(buf, false)]
    s := fmt.Sprintf("%s\n%s", r, buf)
    fmt.Println(s)
}

输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//panic的原因
runtime error: integer divide by zero  
//goroutine的id
goroutine 1 [running]: 
//下面是runtime.Stack方法调用时的调用堆栈链,方法名称和方法被调用的文件行数成对出现
main.printPanicInfo(0x4b78c0, 0x572a10) //方法名称
        E:/xxx/liuwei/test/main.go:29 +0x74 //方法所在的文件和行数
main.f.func1()
        E:/xxx/liuwei/test/main.go:15 +0x59
panic(0x4b78c0, 0x572a10)
        C:/go1.13/go/src/runtime/panic.go:679 +0x1c0 //panic被调用
main.g(...)
        E:/xxx/liuwei/test/main.go:24 //发生panic的代码行数
main.f()
        E:/xxx/liuwei/test/main.go:18 +0x50
main.main()
        E:/xxx/liuwei/test/main.go:9 +0x27

打印的信息中主要由panic原因调用堆栈组成,我们阅读堆栈信息时,可以首先找到runtime.panic,它的下一条堆栈记录就是发生panic的代码具体行数。然后再结合panic的原因信息,一般会很快了解到panic发生的原因。

另外除了panic之外还有一种fatalpanic,这种严重的异常无法使用recover恢复,一般是运行时检测到不可恢复的操作时抛出。例如发生map并发写时会 throw("concurrent map writes"),导致进程崩溃。