Go 异常处理流程

前言

有这样一段代码:

func main() {
    // 捕捉异常
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    go func() {
        fmt.Println("start goroutine")
        panic("error")
    }()
    // 等待
    time.Sleep(time.Second * 100)
}

浅猜一下结果? 没错, 进程崩溃了, defer没有捕捉到异常.

面对这个问题, 可能有的人想的是: "哦, defer不能处理其他goroutine的异常". 但我正巧闲得慌, 索性研究一下Go中异常是如何处理的.

探究

Go语言现在已经实现自举了, 所以其源码也是Go, 不用去看C了. 源码地址: https://github.com/golang/go. 以下源码基于分支release-branch.go1.18

因为当前主要研究的是异常的处理, 所以对协程的原理及defer的调用不做过多探究.

结构体

Go中, 每个协程都有一个结构体来记录其运行信息, 这个结构体的名字是g. 内容大致如下(定义在文件runtime.runtime2.go):

type g struct {
    // ...
  // 当前协程已出发的异常链
  _panic    *_panic
    // 记录当前协程的所有 defer 的链表
    _defer    *_defer
    // ...
}

其他的字段都跳过, 只看_defer字段, 也就是说在Godefer和异常是记录在当前协程中的. 再回到开头的问题, 新启动的协程是没有defer函数的, 自然也就无法捕捉到异常.

既然是研究异常处理, _panic这个结构体自然也得瞅瞅了.

type _panic struct {
    // defer 函数用的, 具体等到研究 defer的时候再看
    argp      unsafe.Pointer
    // 调用 panic 函数时的参数
    arg       any
    // 指向上一个 panic, 组成一个 panic 链表
    // 因为在 panic 处理时, 可能再次发生异常
    // 比如在 defer 函数中发生 panic
    link      *_panic
    // 当前异常是否已经被 recover 处理
    recovered bool
    // 是否被强制终止
    aborted   bool
    goexit    bool
    pc        uintptr
    sp        unsafe.Pointer
}

其中pc sp goexit三个字段, 在defer中发生panic, 然后上层defer对异常进行了recover时使用的, 具体作用我也没整太明白, 可看这次commit 以及这个 issuse

OK, 到这里, 我们和异常的结构体见了一面了, 但异常触发时是如何处理的呢?

处理流程

先上一张流程图来对异常的处理流程进行较为形象的表达.

未命名文件

从上图中, 可以基本看出处理的流程. 不过还是康一下源码吧(为了不影响篇幅, 只保留了部分内容).

你问我怎么知道调用了源码中的哪个函数? 使用命令go tool compile -N -l -S main.go看一下咯.

// 处理异常的函数
func gopanic(e any) {
    gp := getg()
    // ...
    // 创建新的异常并添加到协程的 panic 链表头
    var p _panic
    p.arg = e
    p.link = gp._panic
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    //...
    for {
        d := gp._defer
        //...
        done := true
        // 调用 defer 函数
        // 关于 openDefer, 在这里先按下不表
        if d.openDefer {
            done = runOpenDeferFrame(gp, d)
            if done && !d._panic.recovered {
                addOneOpenDeferFrame(gp, 0, nil)
            }
        } else {
            p.argp = unsafe.Pointer(getargp())
            d.fn()
        }
        //...
        if done {
            //...
            // 将当前 defer 从协程的 defer 链中去掉
            freedefer(d)
        }
        // 异常已经被处理啦
        if p.recovered {
            //...
            // 恢复协程运行. 这里恢复的 recovery 方法不返回
            mcall(recovery)
            throw("recovery failed")
        }
    }
    // 打印 panic 信息
    preprintpanics(gp._panic)
    // 进程终止
    fatalpanic(gp._panic)
    *(*int)(nil) = 0
}
// 当调用 recover 时触发
// 逻辑很简单, 就是从异常链表的头部将异常取出来
// 因为新的异常会放到 _panic 链表头, 所以这里拿到的是最新的异常
func gorecover(argp uintptr) any {
    gp := getg()
    p := gp._panic
    if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
        // 标记当前异常已经被处理
        p.recovered = true
        return p.arg
    }
    // 当前没有异常
    return nil
}

gopanic函数的异常处理流程来看, 异常在gorecover中一旦被处理, 就会将p.recovered标记为true. 而外层一旦检测到其值为true就会恢复运行. 看着貌似没什么问题哈, 但是还记不记得_panic是一个链表呀? 这不是才处理了一个异常么? 如果有其他异常不就丢了么? 比如下面这种情况:

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    defer func() {
        panic("error 2")
    }()
    panic("error")
}

没错, 我们在先调用的defer中再次抛出异常, 此时, error这个异常就没有啦. 现象是符合我们前面的分析的.

系统级异常

在运行时有一些异常是无法通过recover捕获的. 比如调用throw函数的错误, throw函数源码如下:

func throw(s string) {
    systemstack(func() {
        print("fatal error: ", s, "\n")
    })
    gp := getg()
    if gp.m.throwing == 0 {
        gp.m.throwing = 1
    }
    //  直接强制停止
    fatalthrow()
    *(*int)(nil) = 0 // not reached
}

那么哪些操作会触发无法捕获的异常呢? 在系统实现上到处有调用throw函数的, 比如:

  • map的并发读写
  • 占内存耗尽, 比如递归太深
  • go启动的函数为nil

OK, 至此, 虽然在源码层面没有分析的特别细致, 但是对异常的处理流程基本能做到心中有数啦

订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请发表评论。x