1

Panic and recover are also commonly used keywords, which are closely related to the defer mentioned in the previous article. To sum it up in one sentence: after calling panic, it will immediately stop executing the remaining code of the current function, and recursively execute the caller's defer in the current Goroutine; while recover can abort the program crash caused by panic, but it can only play a role in defer .

panic

panic is a built-in function that accepts a parameter of any type. The parameter will be printed when the program crashes. If recovered by recover, the parameter is also the return value of recover. A panic can be triggered explicitly by the programmer, and it can also be triggered at runtime when it encounters unexpected errors such as out of bounds memory.

In the previous article, we know that each Goroutine maintains a _defer linked list (in the case of non-open coding). Every time a defer keyword is encountered during the execution process, a _defer instance will be created and inserted into the linked list. When the function exits, these are taken out at one time. _defer instance and execute. When the panic occurs, it actually triggers the function to exit, that is, the execution flow is turned to the _defer linked list.

There are a few points that need to be clarified during the execution of panic:

  • panic will recursively execute all defers in the current goroutine, and exit after processing is complete;
  • panic will not handle defers in other goroutines;
  • Panic allows multiple calls in defer, the program will terminate the execution of the current defer and continue the previous process.

data structure

The panic keyword is represented in Go by the data structure runtime._panic. Whenever we call panic a data structure like this is created:

 type _panic struct {
    argp      unsafe.Pointer
    arg       interface{}
    link      *_panic
    recovered bool
    aborted   bool
    goexit    bool
}
  • argp is a pointer to the defer function argument;
  • arg is the parameter passed in when calling panic;
  • link points to the previous _panic structure;
  • recovered indicates whether the current _panic is recovered by recover;
  • aborted indicates whether the current _panic is terminated;
  • goexit indicates whether the current _panic is generated by runtime.Goexit.

The _panic linked list, like the _defer linked list, is stored in the Goroutine data structure:

 type g struct {
    // ...
    _panic    *_panic
    _defer    *_defer
    // ...
}

Implementation process

The compiler will convert the keyword panic into the runtime.gopanic function. Let's take a look at its core code:

 func gopanic(e interface{}) {
    gp := getg()
    ...
    var p _panic       // 创建新的 _panic 结构
    p.arg = e          // 存储 panic 的参数
    p.link = gp._panic 
    gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 这两行是将新结构插入到当前 Goroutine 的 panic 链表头部

    for {
        d := gp._defer // 开始遍历 _defer 链表
        if d == nil {
            break
        }

        // 嵌套 panic 的情形
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true // 标记之前 _defer 中的 _panic 为已终止
            }
            // 从链表中删除本 defer
            d._panic = nil
            if !d.openDefer {
                d.fn = nil
                gp._defer = d.link
                freedefer(d)
                continue
            }
        }

        d.started = true // 标记 defer 已经开始执行

        d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) // 标记触发 defer 的 _panic

        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) // 执行 defer 函数,省略对开放编码 _defer 的额外处理

        d._panic = nil
        d.fn = nil
        gp._defer = d.link

        pc := d.pc
        sp := unsafe.Pointer(d.sp)
        freedefer(d)
        // 如果被 recover 恢复的话,处理下面的逻辑
        if p.recovered {
            // ...
            gp._panic = p.link
            for gp._panic != nil && gp._panic.aborted {
                gp._panic = gp._panic.link
            }
            if gp._panic == nil {
                gp.sig = 0
            }

            gp.sigcode0 = uintptr(sp)
            gp.sigcode1 = pc
            mcall(recovery)
            throw("recovery failed") // mcall should not return
        }
    }

    fatalpanic(gp._panic) // 终止整个程序
    *(*int)(nil) = 0
}

The execution of this function includes the following steps:

  1. Create a new runtime._panic and add it to the front of the _panic linked list of the Goroutine;
  2. Determine whether it is the case of nested panic, and carry out relevant marking and processing;
  3. Continuously obtain _defer from the _defer linked list of the current Goroutine and call runtime.reflectcall to run the delayed call function;
  4. Call runtime.fatalpanic to abort the entire program.

recover

recover is also a built-in function to eliminate panic and restore the program to normal. There are also a few points that need to be clarified in the execution process of recover:

  • The return value of recover is the parameter of the eliminated panic;
  • recover must be located directly in the defer function (cannot appear in another nested function) to take effect;
  • After recover successfully handles the exception, the function will not continue to process the logic after panic, but will return directly. For anonymous return values, it will return the corresponding zero value.

Implementation process

The compiler will convert the keyword recover to runtime.gorecover:

 func gorecover(argp uintptr) interface{} {
    gp := getg()
    p := gp._panic
    if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
        p.recovered = true
        return p.arg
    }
    return nil
}

The implementation of the function is very simple, get the _panic instance in the current Goroutine, mark the recovered state of the _panic instance as true if the conditions are met, and then return the parameters of the panic function.

Let's take a look at several valid conditions for recover:

  • p != nil: panic must exist;
  • !p.goexit: not runtime.Goexit();
  • !p.recovered: has not been recovered;
  • argp == uintptr(p.argp): recover must be called directly in defer.

First of all, the panic must exist, the panic generated by runtime.Goexit() cannot be recovered, there is nothing to say about it. Assuming that the function contains multiple defers, after the previous defer eliminates panic through recovery, the recover in the remaining defers cannot be recovered again.

One thing is confusing, the recover function has no parameters, why does the gorecover function have parameters? This is to restrict that recover must be called directly in defer. The parameter of the gorecover function is the parameter address of calling the recover function. The _panic structure stores the parameter address of the current defer function. If the two are consistent, it means that the recover is directly called in the defer. An example is as follows:

 func test() {
    defer func() { // func A
        func() { // func B
            // gorecover 的参数 argp 为 B 的参数地址,p.argp 为 A 的参数的指针
            // argp != p.argp,无法恢复
            if err := recover(); err != nil {
                fmt.Println(err)
            }
        }()
    }()
}

与昊
222 声望634 粉丝

IT民工,主要从事web方向,喜欢研究技术和投资之道