一、panic概述

异常

我们的应用程序常常会出现异常,包括由运行时检测到的异常或者应用开发者自己抛出的异常。

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

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

示例

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

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部分注释,运行结果如下:

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注释部分打开,运行结果为:

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

invalid memory address or nil pointer dereferenceindex out of rangeslice bounds out of rangeinteger divide by zero

追踪panic来源

测试代码

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是怎么产生的:

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

  • 调试自定义panic方法:
  1. 在8行处下断点
  2. 打印main方法的汇编代码
  3. 可以看到panic方法编译后实质是runtime包中的gopanic方法
  • 使用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方法调试:

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

(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"))

(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调试:

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中会调用自身:

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)
}

程序运行结果如下:

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。

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

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:

runtime error: integer divide by zero
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)
}

输出为:

//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发生的原因。

throw("concurrent map writes")