golang_panic_and_recover

panic and recover 解析

  • panic 英文名字是恐慌
    panic 这个词,在英语中具有恐慌、恐慌的等意思。从字面意思理解的话,在 Go 语言中,代表极其严重的问题,程序员最害怕出现的问题。一旦出现,就意味着程序的结束并退出。Go 语言中 panic 关键字主要用于主动抛出异常,类似 java 等语言中的 throw 关键字。
  • recover
    recover 这个词,在英语中具有恢复、复原等意思。从字面意思理解的话,在 Go 语言中,代表将程序状态从严重的错误中恢复到正常状态。Go 语言中 recover 关键字主要用于捕获异常,让程序回到正常状态,类似 java 等语言中的 try ... catch 。

image.png

defer func(){
	err := recover() ;err != nil {
		fmt.Println(err)
	}
}()
 i := []int{5}
 n := i[1]

这里会报错 panic

// gopanic 的代码,在 src/runtime/panic.go 第 454 行

// The implementation of the predeclared function panic.
func gopanic(e interface{}) {
  gp := getg()
  if gp.m.curg != gp {
    print("panic: ")
    printany(e)
    print("\n")
    throw("panic on system stack")
  }

  if gp.m.mallocing != 0 {
    print("panic: ")
    printany(e)
    print("\n")
    throw("panic during malloc")
  }
  if gp.m.preemptoff != "" {
    print("panic: ")
    printany(e)
    print("\n")
    print("preempt off reason: ")
    print(gp.m.preemptoff)
    print("\n")
    throw("panic during preemptoff")
  }
  if gp.m.locks != 0 {
    print("panic: ")
    printany(e)
    print("\n")
    throw("panic holding locks")
  }

  var p _panic
  p.arg = e
  p.link = gp._panic
  gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

  atomic.Xadd(&runningPanicDefers, 1)

  for {
    d := gp._defer
    if d == nil {
      break
    }

    // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
    // take defer off list. The earlier panic or Goexit will not continue running.
    if d.started {
      if d._panic != nil {
        d._panic.aborted = true
      }
      d._panic = nil
      d.fn = nil
      gp._defer = d.link
      freedefer(d)
      continue
    }

    // Mark defer as started, but keep on list, so that traceback
    // can find and update the defer's argument frame if stack growth
    // or a garbage collection happens before reflectcall starts executing d.fn.
    d.started = true

    // Record the panic that is running the defer.
    // If there is a new panic during the deferred call, that panic
    // will find d in the list and will mark d._panic (this panic) aborted.
    d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

    p.argp = unsafe.Pointer(getargp(0))
    reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
    p.argp = nil

    // reflectcall did not panic. Remove d.
    if gp._defer != d {
      throw("bad defer entry in panic")
    }
    d._panic = nil
    d.fn = nil
    gp._defer = d.link

    // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
    //GC()

    pc := d.pc
    sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
    freedefer(d)
    if p.recovered {
      atomic.Xadd(&runningPanicDefers, -1)

      gp._panic = p.link
      // Aborted panics are marked but remain on the g.panic list.
      // Remove them from the list.
      for gp._panic != nil && gp._panic.aborted {
        gp._panic = gp._panic.link
      }
      if gp._panic == nil { // must be done with signal
        gp.sig = 0
      }
      // Pass information about recovering frame to recovery.
      gp.sigcode0 = uintptr(sp)
      gp.sigcode1 = pc
      mcall(recovery)
      throw("recovery failed") // mcall should not return
    }
  }

  // ran out of deferred calls - old-school panic now
  // Because it is unsafe to call arbitrary user code after freezing
  // the world, we call preprintpanics to invoke all necessary Error
  // and String methods to prepare the panic strings before startpanic.
  preprintpanics(gp._panic)

  fatalpanic(gp._panic) // should not return
  *(*int)(nil) = 0      // not reached
  }

通过查看panic 了解主要流程

  • panic 是通过获取当前调用者goroutine
  • 遍历并执行g中的defer
  • 如果defer中发现recover 同时标记为panic 则将panic 转换为recovered
  • 在遍历 defer 的过程中,如果发现已经被标记为 recovered ,则提取出该 defer 的 sp 与 pc,保存在 g 的两个状态码字段中。

总结出来主要就是三步
defer 调用recover
程序触发panic 切换环境到runtime环境并获取在defer中调用recover的g的pc和sp 状态 恢复到g中的状态并处理后面的逻辑

前面提到,panic 函数主要用于主动触发异常。我们在实现业务代码的时候,在程序启动阶段,如果资源初始化出错,可以主动调用 panic 立即结束程序。对于新手来说,这没什么问题,很容易做到。

但是,现实往往是残酷的—— Go 的 runtime 代码中很多地方都调用了 panic 函数,对于不了解 Go 底层实现的新人来说,这无疑是挖了一堆深坑。如果不熟悉这些坑,是不可能写出健壮的 Go 代码。

接下来,笔者给大家细数下都有哪些坑。

数组下标越界

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

访问未初始化的指针或 nil 指针

试图往已经 close 的 chan 里发送数据

并发读写相同 map

package main

import (
  "fmt"
)

func foo(){
  defer func(){
      if err := recover(); err != nil {
          fmt.Println(err)
      }
  }()
  var bar = make(map[int]int)
  go func(){
      defer func(){
          if err := recover(); err != nil {
              fmt.Println(err)
          }
      }()
      for{
          _ = bar[1]
      }
  }()
  for{
      bar[1]=1
  }
}

func main(){
  foo()
  fmt.Println("exit")
}

输出:

fatal error: concurrent map read and map write

goroutine 6 [running]:
runtime.throw(0x10d1f31, 0x21)
        /usr/local/go/src/runtime/panic.go:1116 +0x72 fp=0xc000032770 sp=0xc000032740 pc=0x102ebb2
runtime.mapaccess1_fast64(0x10b28c0, 0xc000056180, 0x1, 0xc000070048)
        /usr/local/go/src/runtime/map_fast64.go:21 +0x196 fp=0xc000032798 sp=0xc000032770 pc=0x100f486
main.foo.func2(0xc000056180)
        /Users/mac/Documents/golang/src/git.tmuyu.com.cn/xin_order/pan/application.go:21 +0x62 fp=0xc0000327d8 sp=0xc000032798 pc=0x109d5f2
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1373 +0x1 fp=0xc0000327e0 sp=0xc0000327d8 pc=0x105b611
created by main.foo
        /Users/mac/Documents/golang/src/git.tmuyu.com.cn/xin_order/pan/application.go:14 +0x6e

goroutine 1 [runnable]:
main.foo()
        /Users/mac/Documents/golang/src/git.tmuyu.com.cn/xin_order/pan/application.go:25 +0x91
main.main()
        /Users/mac/Documents/golang/src/git.tmuyu.com.cn/xin_order/pan/application.go:30 +0x22
exit status 2

这里并发map未加锁
读写错误却没有触发recover

if h.flags&hashWriting != 0 {
  throw("concurrent map read and map write")
}

发现在源码中实际调用的是throw
与前面提到的几种情况不同,runtime 中调用 throw 函数抛出的异常是无法在业务代码中通过 recover 捕获的,这点最为致命。所以,对于并发读写 map 的地方,应该对 map 加锁。

类型断言

package main

import (
    "fmt"
)

func foo(){
    defer func(){
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    var i interface{} = "abc"
    _ = i.([]string)
}

func main(){
    foo()
    fmt.Println("exit")
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×