Background
In the previous article, we learned that panic can occur in three ways:
- Initiated by developers: by calling the
panic()function. - Hidden code generated by the compiler: for example, in the case of division by zero.
- Signals are sent to the process by the kernel, for example, in the case of illegal memory access.
This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.
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 was first 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 process to exit?
- Why does recovery 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 consequences?
Today, let’s dive into the code to clarify these questions.
Based on Go 1.21.4
_panic Data Structure
First, let’s look at an example of an actively triggered panic (example). By examining the assembly code, we can find that panic calls the runtime.gopanic function, which contains a crucial data structure called _panic.
Let’s take a look at the _panic data structure:
|
|
Key fields to focus on:
link: A pointer to the_panicstructure, indicating that_paniccan be a unidirectional linked list, similar to the_deferlinked list.recoveredfield: This field determines whether the_panichas been recovered or not. Therecover()function actually modifies this field.

Let’s also take a look at two important fields in g:
|
|
From this, we can see that both the _defer and _panic linked lists are attached to a goroutine. When can the _panic linked list have multiple elements?
Only when the panic() flow calls the panic() function again within a defer function. This is because the panic() function only executes the _defer functions internally!
The recover() Function
To facilitate explanation, let’s start by analyzing what the recover() function does:
|
|
The recover() function corresponds to the gorecover function implementation in the runtime/panic.go file.
The gorecover Function
|
|
This function is quite simple:
- Retrieve the current goroutine structure.
- Retrieve the latest
_panicfrom the_paniclinked list of the current goroutine. If it is notnil, proceed with the processing. - Set the
recoveredfield of the_panicstructure totrueand return theargfield.
That’s all the recover() function does. It simply sets the value of the recovered field and does not involve any magical code jumps. The setting of the recovered field takes effect within the logic of the panic() function.
The panic() Function
Based on the previous assembly code, we know that panic calls the runtime.gopanic function.
The gopanic Function
The most important part of the panic mechanism is the gopanic function, which contains all the details about panic. The complexity of understanding panic lies in two points:
- Recursive execution of
gopanicwhen panic is nested. - The program counter (pc) and stack pointer (sp) are not manipulated in the usual way, but through direct modification of the instruction register structure, bypassing the logic after
gopanic, and even handling recursivegopaniccalls.
The logic inside gopanic can be divided into two parts: inside the loop and outside the loop.
Inside the Loop
The actions inside the loop can be broken down into the following steps:
- Traverse the
_deferlinked list of the goroutine to retrieve a_deferdeferred function. - Set the
d.startedflag and bind the currentd._panic(used to check during recursion). - Execute the
_deferdeferred function. - Remove the executed
_deferfunction from the linked list. - Check if the
recoveredfield of_panicis set totrueand take appropriate action.- If it is
true, reset the pc and sp registers (generally starting from thedeferreturninstruction) and enqueue the goroutine in the scheduler to wait for execution.
- If it is
Some Considerations
You may notice that the recovered field is only modified in the third step. In fact, you cannot modify the value of _panic.recovered anywhere else.
Question 1: Why does recover need to be placed inside a defer statement to take effect?
Because that is the only opportunity!
Let’s consider a few simple examples:
|
|
In the above example, recover() is called, so why does it still panic?
Because it never reaches the recover() function.