> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Decrypt Go: panic and recover
- URL: https://huizhou92.com/decrypt-go-panic-and-recover-2/
- Published: 2024-02-27T21:22:11.000Z
- Updated: 2026-09-08T02:38:23.000Z
- Description: Decrypt Go: panic and recover. Background In the previous article, we learned that panic can occur in three ways: Initiated by developers: by calling the p。
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

### Background

In the previous article, we learned that panic can occur in three ways:

- Initiated by developers: by calling the `panic()` function.
- Generated by the compiler: for example, in the case of division by zero.
- Sent to the process by the kernel: for example, in the case of an illegal memory access.

All three cases can be categorized as calls to the `panic()` function, indicating that panic in Go is just a special function call and is handled at the language level. Now that we know how panic is triggered, the next step is to understand how panic is handled. When I first started learning Go, I often had some questions in my mind:

- What exactly is panic? Is it a struct or a function?
- Why does panic cause the Go process to exit?
- Why does recover need to be placed inside a defer statement to take effect?
- Even if recover is placed inside a defer statement, why doesn’t the process recover?
- Why is it possible to panic again after a panic? What are the implications?

Today, we will delve into the code to clarify these questions.

> *Based on Go 1.21.4*

### The `_panic` Data Structure

Let’s start by looking at an example of an actively triggered panic. Through the assembly code, we can see that the panic call is made to the [runtime.gopanic](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/panic.go?ref=huizhou92.com#L826) function, which contains a crucial data structure called [\_panic](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/runtime2.go?ref=huizhou92.com#L1035).

Let’s take a look at the `_panic` data structure:

```rust
type _panic struct {   
     argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink   
     arg       any            // argument to panic   
     link      *_panic        // link to earlier panic    pc        uintptr        // where to return to in runtime if this panic is bypassed   
     sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed   
     recovered bool           // whether this panic is over   
     aborted   bool           // the panic was aborted   
     goexit    bool   
 }
```

**Key Fields**:

- `link`: A pointer to the `_panic` structure, indicating that `_panic` can form a unidirectional linked list, similar to the `_defer` list.
- `recovered` field: The recovery of the so-called `_panic` depends on whether this field is set to true. `recover()` modifies this field.

![](https://huizhou92.com/content/images/2026/09/0-q1ausknoyxm5tvd0.png)

Now let’s take a look at two important fields in the [g](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/runtime2.go?ref=huizhou92.com#L414) structure:

```go
type g struct { 
   // ... 
   _panic *_panic // panic linked list, the innermost one 
   _defer *_defer // defer linked list, the innermost one 
   // ... 
 }
```

From here, we can see that both the `_defer` and `_panic` linked lists are attached to the goroutine. When can there be multiple elements on the `_panic` linked list? The answer is when a `panic()` call is made within a defer function. Only in a defer function can an `_panic` linked list be formed because the `panic()` function only executes `_defer` functions!

### The `recover()` Function

For the sake of explanation, let’s start with a simple analysis of what the `recover()` function does:

```go
defer func() { 
   recover() 
 }()
```

The `recover()` function corresponds to the [gorecover](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/panic.go?ref=huizhou92.com#L1038) function implementation in the `runtime/panic.go` file.

### The `gorecover` Function

```go
func gorecover(argp uintptr) any { 
   // Must be in a function running as part of a deferred call during the panic. 
   // Must be called from the topmost function of the call 
   // (the function used in the defer statement). 
   // p.argp is the argument pointer of that topmost deferred function call. 
   // Compare against argp reported by caller. 
   // If they match, the caller is the one who can recover. 
   gp := getg() 
   p := gp._panic 
   if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) { 
     p.recovered = true 
     return p.arg 
   } 
   return nil 
 }
```

This function is quite simple:

1. Retrieve the current goroutine structure.
2. Get the latest `_panic` from the `_panic` linked list of the current goroutine. If it is not `nil`, proceed with the processing.
3. Set the `recovered` field of the `_panic` structure to true and return the `arg` field.

That’s all there is to the `recover()` function. It simply sets the value of the `recovered` field in the `_panic` structure and does not involve any magical code jumps. The assignment of `recovered` is effective within the logic of the `panic()` function.

### The `panic()` Function

Based on the previous assembly code, we know that the panic call is made to the [runtime.gopanic](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/panic.go?ref=huizhou92.com#L826) function.

### The `gopanic` Function

The most important part of the panic mechanism is the `gopanic` function. All the details about panic are in this function. The complexity of understanding panic lies in two points:

1. Recursive execution of `gopanic` when panic is nested.
2. The program counter (pc) and stack pointer (sp) are not manipulated in the usual way of function call and return. During recovery, the instruction registers are modified directly, bypassing the remaining logic of `gopanic` and even the recursive logic of multiple `gopanic` calls.

We can understand the logic of `gopanic` by dividing it into two parts: inside the loop and outside the loop.

#### Inside the Loop

The actions inside the loop can be divided into the following steps:

1. Iterate through the defer linked list of the goroutine and retrieve a `_defer` deferred function.
2. Set the `d.started` flag and bind the current `d._panic` (used for recursive detection).
3. Execute the `_defer` deferred function.
4. Remove the executed `_defer` function from the linked list.
5. Check if the `recovered` field of the `_panic` structure is set to true and take appropriate action.  
If it is true, reset the pc and sp registers (usually starting from the deferreturn instruction) and enqueue the goroutine in the scheduler for execution.
6. Repeat the above steps.

#### Questions to Consider

You may notice that the `recovered` field is only modified during the third step. It cannot be modified anywhere else. **Question 1: Why does recover need to be placed inside a defer statement to take effect?** Because that's the only opportunity!

Let’s consider a few straightforward examples:

```go
func main() { 
   panic("test") 
   recover() 
 }
```

In the above example, `recover()` is called after `panic()`. Why does it still panic? Because the `recover()` function is never executed. The execution order is as follows:

```
panic 
   -> gopanic 
      -> Execute the defer linked list 
         -> exit
```

Someone might argue, “What if I put `recover()` before `panic('test')`?"

```go
func main() { 
   recover() 
   panic("test") 
 }
```

No, it won’t work because when `recover()` is executed, the `_panic` is not attached to the goroutine yet. So `recover()` is useless in this case.

**Question 2: Even if recover is placed inside a defer statement, why doesn’t the process recover?** Let’s recall the operations in the for loop:

```go
// Step: Iterate through the _defer linked list 
 d := gp._defer 
 // Step: Execute the defer function 
 reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) 
 // Step: Remove the executed defer function 
 gp._defer = d.link
```

**Key Point: In the `gopanic` function, only the `_defer` functions of the current goroutine are executed. Therefore, if a `recover()` is performed in a defer function attached to another goroutine, it will have no effect.**

Let’s see an example:

```go
func main() { // g1 
    go func() { // g2 
        defer func() { 
            recover() 
        }() 
    }() 
    panic("test") 
}
```

Since the `panic` and `recover` are in two different goroutines, the `_panic` is attached to g1, and the `recover` in g2's `_defer` chain cannot access the `_panic` structure of g1\. Therefore, it cannot set the `recovered` field to true, and the program still crashes.

**Question 3: Why is it possible to panic again after a panic? What are the implications?** This is actually quite easy to understand. Some people might overthink it. Can we call `panic()` recursively? Yes, we can. The scenario is usually as follows:

- The `gopanic` function calls a `_defer` function.
- The `_defer` function calls `panic()` or `gopanic()`.

This is just a simple function recursion, and there’s nothing special about it. In this scenario, an `_panic` linked list will be formed starting from `gp._panic`. The instructions executed by `gopanic` are special in two ways:

1. If the `_panic` structure is set to recovered, the pc and sp registers are reset, bypassing `gopanic` (including nested function stacks), and jumping directly to the instruction to be executed after the defer function (deferreturn).
2. If there is no handler for the `_panic` data, exit the process and terminate the execution of subsequent instructions.

Let’s look at an example of nested panic:

```go
func main() { 
    defer func() { // defer_0 
        panic("panic again") 
    }() 
    panic("first") 
}
```

The function execution is as follows:

```
gopanic // First panic 
        defer_0 is executed 
            gopanic // Second panic 
                defer_0 is removed from the linked list (recursive call), termination condition is met 
                 
    // Print stack trace and exit the program 
    fatalpanic
```

Here’s another comparison example:

```go
func main() { 
    println("=== begin ===") 
    defer func() { // defer_0 
        println("=== come in defer_0 ===") 
    }() 
    defer func() { // defer_1 
        recover() 
    }() 
    defer func() { // defer_2 
        panic("panic 2") 
    }() 
    panic("panic 1") 
    println("=== end ===") 
}
```

Will this function print the stack trace and exit? **The answer is no.** The output will be:

```bash
➜ panic ./test_panic === begin === === come in defer_0 ===
```

Did you guess it correctly? Let me explain the complete route:

```sql
main 
    gopanic // First panic 
        1. Retrieve defer_2, set started 
        2. Execute defer_2 
            gopanic // Second panic 
                1. Retrieve defer_2, set panic as aborted 
                2. Remove defer_2 from the linked list 
                3. Execute defer_1 
                    - Execute recover 
                4. Remove defer_1 
                5. Execute recovery, reset pc register, jump to the instruction registered in defer_1 (usually deferreturn)
```

// Jump out of the recursive call of gopanic, directly to the execution of deferreturn;  
 defereturn  
 1\. Iterate through the defer function chain, there is still defer\_0 left, retrieve defer\_0  
 2\. Execute defer\_0  
 // End of the main function

Here’s another example:

```go
func main() { 
    println("=== begin ===") 
    defer func() { // defer_0 
        println("=== come in defer_0 ===") 
    }() 
    defer func() { // defer_1 
        panic("panic 2")     
    }() 
    defer func() { // defer_2 
        recover() 
    }() 
    panic("panic 1") 
    println("=== end ===") 
}
```

Will this function print the stack trace and exit? **The answer is yes.** The output will be:

```go
➜ panic ./test_panic === begin === === come in defer_0 === panic: panic 2 goroutine 1 [running]: main.main.func2() /Users/code/gopher/src/panic/test_panic.go:9 +0x39 main.main() /Users/code/gopher/src/panic/test_panic.go:11 +0xf7
```

The execution path is as follows:

```sql
main 
    gopanic // First panic 
        1. Retrieve defer_2, set started 
        2. Execute defer_2  
            - Execute recover, set panic_1 as recovered 
        3. Remove defer_2 from the linked list 
        4. Execute recovery, reset pc register, jump to the instruction registered in defer_1 (usually deferreturn) 
 
    // Jump out of the recursive call of gopanic, execute deferreturn; 
    defereturn 
 
        1. Iterate through the defer function chain, retrieve defer_1 
        2. Remove defer_1 
        2. Execute defer_1 
            gopanic // Second panic 
                1. There is a defer_0 on the defer chain 
                2. Execute defer_0 (defer_0 does not recover, only prints one line of output) 
                3. Remove defer_0, the chain is empty, exit the for loop 
                3. Execute fatalpanic 
                    - exit(2) terminates the process
```

Did you guess correctly?

### The `recovery` Function

Finally, let’s take a look at the crucial `recovery` function. In the `gopanic` function, when executing the defer functions in the loop, if the `recovered` field of the `_panic` structure is set to true, the `mcall(recovery)` function is called to perform the so-called "recovery."

Let’s take a look at the implementation of the `recovery` function. It's a very simple function that resets the pc and sp registers and reschedules the Goroutine for execution.

```go
// runtime/panic.go 
func recovery(gp *g) { 
    // Retrieve the values of the stack pointer and program counter 
    sp := gp.sigcode0 
    pc := gp.sigcode1 
    // Reset the pc and sp registers of the Goroutine 
    gp.sched.sp = sp 
    gp.sched.pc = pc 
    // Reschedule the Goroutine 
    gogo(&gp.sched) 
}
```

Resetting the pc and sp registers means what? The pc register points to the address of the instruction, in other words, it jumps to another location to execute instructions. It no longer executes the instructions sequentially after `gopanic`. The `_defer.pc` is the instruction line of the executed code, and where is this instruction?

For this, let’s recall the chapter on `defer`. When a deferred function is registered, it corresponds to a `_defer` structure. When creating this structure, the `_defer.pc` field is assigned the instruction on the next line after the `new` function. This was explained in detail in the chapter on "In-Depth Analysis of Defer".

Here’s an example: if it’s allocated on the stack, it will be in `deferprocStack`. So, `mcall(recovery)` jumps to this position, and the subsequent logic follows the `deferreturn` logic, executing the remaining `_defer` function chain.

> That concludes the explanation of panic. It’s just a special function call, nothing special. The only thing that makes it special is the special instruction jumps it performs.

[文章索引](/article-index/)