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_panic
structure, indicating that_panic
can be a unidirectional linked list, similar to the_defer
linked list.recovered
field: This field determines whether the_panic
has 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
_panic
from the_panic
linked list of the current goroutine. If it is notnil
, proceed with the processing. - Set the
recovered
field of the_panic
structure totrue
and return thearg
field.
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
gopanic
when 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 recursivegopanic
calls.
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
_defer
linked list of the goroutine to retrieve a_defer
deferred function. - Set the
d.started
flag and bind the currentd._panic
(used to check during recursion). - Execute the
_defer
deferred function. - Remove the executed
_defer
function from the linked list. - Check if the
recovered
field of_panic
is set totrue
and take appropriate action.- If it is
true
, reset the pc and sp registers (generally starting from thedeferreturn
instruction) 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.