Panic and recover mechanism has been introduced before, and several panic/recover use cases are shown in the last article. This current article will explain panic/recover mechanism in detail. Exiting phases of function calls will also be explained detailedly.

A funciton call may enter its exiting phase (or exit directly) through three ways:For example, in the following code snippet,

In Go, a function call may undergo an exiting phase before it fully exits. In the exiting phase, the deferred function calls pushed into the defer-call stack in executing the function call will be executed (in the inverse pushing order). When all of the deferred calls fully exit, the exiting phase ends and the function call also fully exits.

When a panic occurs directly in a function call, we say the (unrecovered) panic starts associating with the function call. Similarly, when the runtime.Goexit function is called in a function call, we say a Goexit signal starts associating with the function call after the the runtime.Goexit call fully exits. A panic and a Goexit signal are independent of each other. As explained in the last section, associating either a panic or a Goexit signal with a funciton call will make the function call enter its exiting phase immediately.

We have learned that panics can be recovered. However, there are no ways to cancel a Goexit signal.

the call will associate with no panics when the unrecovered panic is recovered.

when a new panic occurs in the function call, the new one will replace the old one to be the associating unrecovered panic of the function call.

main

package main import "fmt" func main() { defer func() { fmt.Println(recover()) // 3 }() defer panic(3) // will replace panic 2 defer panic(2) // will replace panic 1 defer panic(1) // will replace panic 0 panic(0) }

At any give time, a function call may associate with at most one unrecovered panic. If a call is associating with an unrecovered panic, thenFor example, in the following program, the recovered panic is panic 3, which is the last panic associating with thefunction call.

As Goexit signals can't be cancelled, arguing whether a function call may associate with at most one or more than one Goexit signal is unnecessary.

if there was an old unrecovered panic associating with the nesting call before, the old one will be replaced by the spread one. For this case, the nesting call has already entered its exiting phase for sure, so the next deferred function call in the defer-call stack will be invoked.

if there was not an unrecovered panic associating with the nesting call before, the spread one will associates with the the nesting call. For this case, the nesting call might has entered its exiting phase or not. If it hasn't, it will enter its exiting phase immediately.

Although it is unusual, there might be multiple unrecovered panics coexisting in a goroutine at a time. Each one associates with one non-exited function call in the call stack of the goroutine. When a nested call still associating with an unrecovered panic fully exits, the unrecovered panic will spread to the nesting call (the caller of the nested call). The effect is the same as a panic occurs directly in the nesting call. That says,

So, when a goroutine finishes to exit, there may be at most one unrecovered panic in the goroutine. If a goroutine exits with an unrecovered panic, the whole program crashes. The information of the unrecovered panic will be reported when the program crashes.

When a function is invoked, there is neither a panic nor Goexit signals associating with its call initially, no matter whether its caller (the nesting call) has entered exiting phase or not. Surely, panics might occur or the runtime.Goexit function might be called later in the process of executing the call, so panics and Goexit signals might associate with the call later.

package main func main() { // The new goroutine. go func() { // The anonymous deferred call. // When it fully exits, the panic 2 will spread // to the entry function call of the new // goroutine, and replace the panic 0. The // panic 2 will never be recovered. defer func() { // As explained in the last example, // panic 2 will replace panic 1. defer panic(2) // When the anonymous function call fully // exits, panic 1 will spread to (and // associate with) the nesting anonymous // deferred call. func () { panic(1) // Once the panic 1 occurs, there will // be two unrecovered panics coexisting // in the new goroutine. One (panic 0) // associates with the entry function // call of the new goroutine, the other // (panic 1) accosiates with the // current anonymous function call. }() }() panic(0) }() select{} }

panic: 0 panic: 1 panic: 2 goroutine 5 [running]: ...

The following example program will crash if it runs, because the panic 2 is still not recovered when the new goroutine exits.The output (when the above program is compiled with the standad Go compiler v1.15):

The format of the output is not perfect, it is prone to make some people think that the panic 0 is the final unrecovered panic, whereas the final unrecovered panic is panic 2 actually.

Similarly, when a nested call fully exits and it is associating with a Goexit signal, then the Goexit signal will also spread to (and associate with) the nesting call. This will make the nesting call enter (if it hasn't entered) its exiting phase immediately.

package main import "runtime" func f() { // The Goexit signal shadows the "bye" // panic now, but it should not. defer runtime.Goexit() panic("bye") } func main() { go f() for runtime.NumGoroutine() > 1 { runtime.Gosched() } }

The above has mentioned that a panic and a Goexit signal are independent of each other. In other words, an unrecovered panic should not cancel a Goexit signal, and a Goexit signal should not shadow an unrecovered panic or be cancelled. However, both of the current official standard Go compiler (gc, v1.15) and gccgo (v8.0) don't implement this rule correctly. For example, the following program should crash but it doesn't if it is compiled with the current versions of gc and gccgo.

The problem will be fixed in future versions of gc and gccgo.

package main import "runtime" func f() { defer func() { recover() }() defer panic("will cancel Goexit but should not") runtime.Goexit() } func main() { c := make(chan struct{}) go func() { defer close(c) f() for { runtime.Gosched() } }()

The following example program should exit quickly in running, but it will not compile correctly with the current gccgo version (v8.0) and gc versions before Go Toolchain 1.14. In fact, it never exits if it compiles with those compiler versions.

Since Go Toolchain 1.14, the problem has been fixed in the standard compiler (gc).