Panic and Recover in the Go Runtime
Tags: go, programming, coding
Published: 2024-05-03 23:58:22

_Art by [Ashley McNamara](https://github.com/ashleymcnamara/gophers)_

Go developers are well aware of how the built-in `panic` and `recover` funcs behave. But most of us haven’t dug into the Go source to understand why they behave the way they do. Why does `recover` need to be deferred? Why is a panic only recoverable within the goroutine it occurred? To answer these questions, we need to understand the Go runtime.

## The Go Runtime

The Go runtime is composed of code to be executed (goroutines), OS threads, and runtime resources such as scheduler and memory allocator state. The Go source [refers to these](https://github.com/golang/go/blob/master/src/runtime/HACKING.md#gs-ms-ps) in the runtime pkg as **G**, **M**, and **P**, respectively. Each **M** has a system stack associated with it (referred to as `g0` in the source). The Go runtime executes on the user stack unless making explicit calls to [mcall](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/stubs.go#L38) or [systemstack](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/stubs.go#L58) funcs. The behaviour of `getg` can also tell us when the runtime is executing on user or system stack.

Each [goroutine](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/runtime2.go#L422) has a user stack associated with it and, importantly, all values of the [panic struct](https://github.com/golang/go/blob/2e1003e2f7e42efc5771812b9ee6ed264803796c/src/runtime/runtime2.go#L1013) reside on this stack.

## Digging into the Go Runtime Source

Panics are initiated via the [gopanic](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L709) func. Panic structs are simply initialized with the argument supplied to the `panic()` call:

```go
var p _panic
p.arg = e
```

If the panic occurred on a user stack, the panic procedure begins [here](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L753) with reference to the caller's program counter and stack pointer:

```go
p.start(getcallerpc(), unsafe.Pointer(getcallersp()))
```

The goroutine is assigned a panic reference from the panic struct’s start() method [here](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L813):

```go
gp._panic = (*_panic)(noescape(unsafe.Pointer(p)))
```

After this, the user stack is unwound while looking for deferred function calls. If a stack frame with deferred func calls is found, the panic struct is assigned the relevant pointers for the continued unwinding and recovery process. The stack unwinding executes on the system stack [here](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L939):

```go
func (p *_panic) nextFrame() (ok bool) {
...
gp := getg()
...
	systemstack(func() {
...
		var u unwinder
		u.initAt(p.lr, uintptr(p.fp), 0, gp, 0)
...
		p.lr = u.frame.lr
		p.sp = unsafe.Pointer(u.frame.sp)
		p.fp = unsafe.Pointer(u.frame.fp)
```

The panic process then calls all the deferred funcs from the relevant stack frame [here](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L755).

```go
for {
	fn, ok := p.nextDefer()
	if !ok {
		break
	}
	fn()
}
```

Eventually, the [nextDefer](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L830) func will return the builtin `recover()` which is implemented [here](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L982), as `gorecover()`. The first thing that `gorecover()` does is get a reference to the relevant panic:

```go
func gorecover(argp uintptr) any {
...
	gp := getg()
	p := gp._panic
```

The panic struct contains an argument pointer of the topmost deferred function call (`p.argp`, assigned in `nextDefer()` [here](https://github.com/golang/go/blob/d49a14c6092ba30d0afbdaa06a24016ff1fe2a82/src/runtime/panic.go#L846)). The `gorecover()` func checks that its caller has matched this pointer and executes the recover by returning the argument supplied to the panic call (`p.arg`, which was assigned to the panic in `gopanic()` [here](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L749)).

```go
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
	p.recovered = true
	return p.arg
}
```

Once the panic is in a recovered state (`p.recovered`), it will [begin](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L839) a [recovery](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L1061) process on the system stack whereby the Go runtime ensures that the caller of the recovered func receives results as if the panic had never happened.

## Summarising the Go Runtime Source

The source we covered above is overwhelming on first pass. The key takeaways are the following:

* The Go runtime executes goroutines (**G**) on OS threads (**M**);

* Goroutines have their own user stack space;

* The Go runtime manages panic instances with reference to values on the panicking goroutines’ stack space;

* The Go runtime uses the system stack to unwind the user stack frames;

* The Go runtime executes deferred funcs during the stack unwinding process; and

* The Go runtime initiates a “recovery” process if one of the deferred funcs was the built-in `recover()`.

Now that we have an understanding of what happens under the hood, lets see it in action!

## Using the Assembler

We can look at assembly of Go programs using [godbolt.org](https://godbolt.org/). Here is a simple program that panics:

```go
package main

func proc() {
    defer func() {
        if r := recover(); r != nil {
            println(r)
        }
    }()
    panic(1)
}

func main() {
    proc()
}
```

If we look at the assembly of this program, we can see the expected runtime calls we covered above.

```
proc_pc0:
...
        CALL    runtime.gopanic(SB)
...
        CALL    runtime.deferreturn(SB)

proc_func1_pc0:
...
        CALL    runtime.gorecover(SB)
```

The compiler inserts a call to [deferreturn](https://github.com/golang/go/blob/63deaf00ea6058d1422f0b435e475666cba5743e/src/runtime/panic.go#L581) at the end of all funcs make defer calls. This func uses the panic struct for its purposes, but it has a different flow to the one we covered above.

The **SB** (static-base pointer) symbol is a virtual register that points to the beginning of the address space of the program. This symbol can be used to jump to function addresses, which is what the `CALL` instructions above are doing. If you are interested in understanding the Go assembler in greater detail, go.dev provides [this](https://go.dev/doc/asm) primer.

## Thanks!

Thanks for taking your time to read this article. This article was initially intended as an addendum to the Panic, Recover, and Relax article published beforehand. However, while digging into the implementation inside the Go runtime, it became clear that there was a lot to explain!