Why is panic worth thinking about?
When learning Go, many questions often arise. Sometimes, what seems to be understood is actually not. What exactly is panic? It seems obvious, but it can be difficult to explain. I’m going to use two articles to understand the concept of panic thoroughly:
- Posture: Understanding the origins of panic. It doesn’t just come out of nowhere. There are three main postures to consider.
- Principles: Fully comprehending the internal workings of panic and understanding its deeper principles.
This article is first published in the medium MPP plan. If you are a medium user, please follow me in medium. Thank you very much.
The Three Postures of Panic
When does panic occur? Let’s start with the “form.” From a developer’s perspective, panic can be categorized into active and passive.
Active posture:
- Developers actively call the
panic()
function.
Passive posture:
- The compiler triggers hidden code.
- The kernel sends a signal to the process.
Hidden Code by the Compiler
Go is simple yet powerful, and the compiler plays a crucial role. It handles many tasks on behalf of programmers, such as logic supplementation and memory escape analysis. This includes the throwing of panics!
Let’s take a classic example: dividing by zero in integer arithmetic triggers a panic. How does this happen?
Consider the following minimal code snippet:
|
|
This function has a risk of division by zero. When b
is equal to 0, the program triggers a panic and exits, as shown below:
|
|
Now, the question is: How does the program trigger a panic?
Code reveals all secrets.
Looking at the code, it seems simple—just one line: c := a / b
, right?
Well, it’s actually assembly code. The hidden logic added by the compiler is not visible in the source code.
By using the dlv debugger to set a breakpoint in the divzero
function and executing disassemble
, we can uncover the secret. Here is a snippet of the assembly code with annotations:
|
|
Do you see the hidden function?
The compiler secretly adds an if/else
logic and even includes the code for runtime.panicdivide
.
- If
b
is equal to 0, it jumps to the functionruntime.panicdivide
.
Take a look at thepanicdivide
function. It’s a simplified wrapper:
|
|
As you can see, it calls the panic()
function.
This is how a panic is triggered when dividing by zero. It’s not something that magically appears out of nowhere; rather, it’s the additional logic added by the compiler to ensure that a panic is triggered when the divisor is 0.
Triggered by Process Signal
The most typical example is illegal memory access, such as accessing a nil pointer, which triggers a panic. How does this happen?
Consider this minimal example:
|
|
When calling nilptr(nil)
, it will cause the process to exit with an exception:
|
|
Now, the question is: How does this panic occur?
When the Go process starts, it registers the default signal handler, sigtramp
. When the CPU encounters a 0 address, it triggers a page fault exception, indicating an illegal address. The kernel sends a SIGSEGV
signal to the process. When this signal is received, the sigtramp
function handles it, ultimately calling the panic()
function:
|
|
In the sigpanic
function, the panicmem
function is called, which then calls the panic()
function. This leads to Go’s panic handling.
Similar to panicdivide
, panicmem
is a minimal wrapper for panic()
:
|
|
This method triggers the panic function by using a software interrupt via a signal. It allows the Go registered signal handler to be invoked, enabling panic handling at the language level.
You may wonder when the logic for signal handling is registered.
During process initialization, when creating M0 (thread), the system call sigaction
is used to register the signal handling function as sigtramp
. The call stack looks like this:
|
|
As a result, when a software interrupt is triggered, the Go signal handling function is called, allowing panic handling at the language level.
Active Panic by Developers
The third way is when developers actively call panic
themselves:
|
|
This is a simple function call—very straightforward.
Discussing the Essence of Panic
Now that we have explored the postures of panic, all three methods ultimately rely on the panic()
function. So, one thing is clear: panic is a language-level handling mechanism.
By default, after a panic occurs, if Go doesn’t handle it in any specific way, the default behavior is to print the reason for the panic, print the stack trace, and exit the program.
Now, let’s go back to the fundamental question: What exactly is panic?
I won’t delve into the concept, but I’ll describe a few simple facts:
- The
panic()
function internally creates a crucial data structure called_panic
and associates it with the goroutine. - The
panic()
function executes the_defer
function chain and handles the state of_panic
accordingly.
What does it mean to handle _panic
?
It means looping through the _defer
function chain on the goroutine. If all the _defer
functions are executed and the state of _panic
hasn’t been recovered, there is no other option but to exit the process and print the stack trace.
If a friend on the _defer
chain recovers the state of _panic
, marking it as recovered, the process ends there. The normal code execution continues after that, following the deferreturn
logic.
So, what is panic?
It’s just a special function call. That’s all it is.
How special is it? I will explore its deep principles in the next article. In the meantime, consider a few questions:
- What exactly is panic? Is it a structure or a function?
- Why does panic cause a Go process to exit?
- Why does recover need to be placed within a defer to work?
- Why does the process still not recover even when recover is placed within a defer?
- Why is it possible to panic again after a panic? What are the consequences?
Summary
- Panic can occur in three ways: it can be actively triggered by developers, assisted by the compiler’s logic, or triggered by a software interrupt signal.
- Regardless of the posture, all methods ultimately rely on the
panic()
function. Panic is merely a language-level handling mechanism. - By default, after a panic occurs, if not handled, the program prints the panic cause, the stack trace, and exits the process.