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

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

源代码:


recoverpanicpanic
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))
}
task
task
append

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

recover

执行代码后输出:

[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个任务仍然正常的执行了!

D:/workspace/Go_Learn/app.go: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()
}()

源代码: