要解决的问题

  • 如何实现golang程序panic时的自救

解决方案

  • 对Go语言panic相关原理进行剖析,以尽可能优雅地在程序panic发生时进行自救。

Go语言panic相关原理

panic发生时程序控制权如何变更

  • golang使用调用栈维护函数间调用关系
  • 每当发生函数调用,即封装当前函数运行信息并压入调用栈,并转移程序控制权给被调用函数
  • 每当代码产生panic,即从调用栈弹出其上一级调用函数,并转移程序控制权给调用函数
  • 程序控制权沿调用栈的反方向,逐级传播至最外层调用函数——go函数或main函数

Go语言内建的recover()函数

  • 调用时如果程序未处于panic,则返回nil,对程序控制权无影响
  • 调用时如果程序处于panic,则返回panic信息,并重新获得程序控制权
  • 使用方式:与defer语句联用

defer语句

  • 作用:延迟与defer语句联用的函数代码的执行
  • 延迟时机:defer语句所属函数即将结束执行的那一刻,无论是否产生panic
  • 前置条件:在函数产生panic之前,defer语句已经执行

解决方案案例

单goroutine场景

  • recover()恢复 + 日志打印
func genPanic() {
    logger.Info("Enter function genPanic")

    defer func() {
        if panicInfo := recover(); panicInfo != nil {
            logger.Warn("There is a panic in genPanic:", panicInfo)
        }
    }()

    // 引发panic
    nums := make([]int, 0)
    fmt.Println(nums[1])
}

源代码路径:../../golang-recover-from-panics/recover&log/demo.go

  • recover() + log + 异常封装成错误
func FuncA(nums []int) {
	if nums == nil {
		panic("nums == nil")
	}
	fmt.Println(nums[1])
}

func Process() (err error) {
	logger.Info("Enter function Process")

	defer func() {
		if panicInfo := recover(); panicInfo != nil {
			// log
			logger.Warn("There is a panic in Process:", panicInfo)
			panicInfoMsg := fmt.Sprintf("%s", panicInfo)
			if strings.Contains(panicInfoMsg, "nil") {
				// return err
				err = errors.New("FucA panic: nums == nil")
			}
		}
	}()

	// 引发panic
	FuncA(nil)

	logger.Info("Exit function Process")

	return
}

源代码路径:../../golang-recover-from-panics/recover&log&error/demo.go

多goroutine场景(异常监听)

  • recover() + log + 异常封装成错误 + channel
func FuncA(nums []int) {
	if nums == nil {
		panic("nums == nil")
	}
	fmt.Println(nums[1])
}

type ErrorProcess struct {
	Error      error
	ProcessNum int
}

func Process(processNum int, errChan chan ErrorProcess) (err error) {
	logger.Info("Enter function Process")

	defer func() {
		if panicInfo := recover(); panicInfo != nil {
			// log
			logger.Warn("There is a panic in Process:", panicInfo)
			panicInfoMsg := fmt.Sprintf("%s", panicInfo)
			if strings.Contains(panicInfoMsg, "nil") {
				// return err
				err = errors.New("FucA panic: nums == nil")
				errChan <- ErrorProcess{err, processNum}
			}
		}
	}()

	// 引发panic
	FuncA(nil)

	logger.Info("Exit function Process")

	return
}

func main() {
	errChan := make(chan ErrorProcess)
	for i := 0; i < 10; i++ {
		go Process(i, errChan)
	}

	for {
		errProcess, ok := <-errChan
		if ok {
			fmt.Printf("err is not nil: %v\n", errProcess)
		} else {
			fmt.Printf("All process is finished")
			break
		}
	}

}

源代码路径:../../golang-recover-from-panics/recover&log&channel/demo.go