文章506
标签266
分类65

在Golang发生Panic后打印出堆栈信息

虽然用了比较长时间的Golang,但是还是有很多不懂得地方;比如,最近我才发现,原来通过recover函数拦截的err并不会返回堆栈信息,而是仅仅返回类似于“空指针错误”的信息,基本上没什么用,更没法定位到底是哪行代码发生了panic十分鸡肋;

最后经过查找网上的资料发现,可以通过runtime包获取到堆栈信息;

源代码:


在Golang发生Panic后打印出堆栈信息

对于实际的项目来说,框架都会提供recover来做业务发生panic时的拦截,保证整个服务不会因为一个业务的panic而导致整个服务直接挂掉;

同时,通常情况下框架都会记录并打出panic的堆栈信息,但是在框架之外,我们该怎么打印出来堆栈信息呢?

其实很简单通过runtime.Stack函数即可!

下面的三行代码就能返回当前Goroutine的堆栈信息:

// getCurrentGoroutineStack 获取当前Goroutine的调用栈,便于排查panic异常
func getCurrentGoroutineStack() string {
    var buf [defaultStackSize]byte
    n := runtime.Stack(buf[:], false)
    return string(buf[:n])
}

下面看一个实际项目抽象出的例子:

package main

import (
    "fmt"
    "runtime"
    "sync"
)

const (
    defaultStackSize = 4096
)

func callPanic() {
    panic("test panic")
}

// getCurrentGoroutineStack 获取当前Goroutine的调用栈,便于排查panic异常
func getCurrentGoroutineStack() string {
    var buf [defaultStackSize]byte
    n := runtime.Stack(buf[:], false)
    return string(buf[:n])
}

func task(arr *[]int, i int, wg *sync.WaitGroup, lock *sync.Mutex) {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("[panic] err: %v\nstack: %s\n", err, getCurrentGoroutineStack())
        }
        wg.Done()
    }()

    if i == 500 {
        callPanic()
    }

    lock.Lock()
    defer lock.Unlock()
    *arr = append(*arr, i)
}

func main() {
    wg := sync.WaitGroup{}
    lock := sync.Mutex{}

    arr := make([]int, 0)
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go task(&arr, i, &wg, &lock)
    }
    wg.Wait()

    fmt.Println(len(arr))
}

在main函数中,会并发的创建10000个task任务;

在每个task任务中,会向arr数组的末尾添加一个 i 值;

注:Golang中内置的append函数是非线程安全的!

同时,当 i 为500时,代码模拟了业务panic的场景;

并且,为了防止单个 task 的 panic 影响到其他任务,我们在每一个 task 任务的开头都声明了defer函数,在其中使用recover对panic进行了拦截;

执行代码后输出:

[panic] err: test panic
stack: goroutine 507 [running]:
main.getCurrentGoroutineStack(...)
    D:/workspace/Go_Learn/app.go:20
main.task.func1(0xc000010090)
    D:/workspace/Go_Learn/app.go:27 +0xc5
panic(0x963180, 0x99cfa0)
    E:/golang/src/runtime/panic.go:969 +0x176
main.callPanic(...)
    D:/workspace/Go_Learn/app.go:14
main.task(0xc000004480, 0x1f4, 0xc000010090, 0xc0000100a0)
    D:/workspace/Go_Learn/app.go:33 +0x197
created by main.main
    D:/workspace/Go_Learn/app.go:48 +0x10f

9999

可以看到单个 task 的 panic 并不会影响到其他 task:对于添加10000个数的任务,单个任务panic后,其他的9999个任务仍然正常的执行了!

同时,我们可以很容易的定位到,Panic 来源于 D:/workspace/Go_Learn/app.go:14,即代码的第14行!


总结

对于并发的情况,对于 task 的抽象是非常重要的;

同时,对于每一个单独的并发 task,都推荐采用下面的代码来对 panic 进行拦截,防止一个 task 的 panic 影响到其他所有的 task;

并且,为每一个 task 在 panic 时打印出堆栈来直接定位问题,并保证 WaitGroup 能够正常退出;

defer func() {
    if err := recover(); err != nil {
        fmt.Printf("[panic] err: %v\nstack: %s\n", err, getCurrentGoroutineStack())
    }
    wg.Done()
}()

附录

源代码:



本文作者:Jasonkay
本文链接:https://jasonkayzk.github.io/2021/09/26/在Golang发生Panic后打印出堆栈信息/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可