Go Panic and Recover Explained in Depth: Everything You Need to Know!
James Reed
Infrastructure Engineer · Leapcell

Detailed Explanation of the panic and recover Keywords in the Go Language
In the Go language, there are two keywords that often appear in pairs — panic and recover. These two keywords are closely related to defer. They are both built-in functions in the Go language and provide complementary functions.
I. Basic Functions of panic and recover
- panic: It can change the control flow of the program. After calling panic, the remaining code of the current function will be immediately stopped from execution, and the defer of the caller will be recursively executed in the current Goroutine.
- recover: It can stop the program crash caused by panic. It is a function that can only take effect in defer. Calling it in other scopes will not have any effect.
II. Phenomena When Using panic and recover
(I) panic Only Triggers the defer of the Current Goroutine
The following code demonstrates this phenomenon:
func main() { defer println("in main") go func() { defer println("in goroutine") panic("") }() time.Sleep(1 * time.Second) }
The running result is as follows:
$ go run main.go
in goroutine
panic:
...
When running this code, it will be found that the defer statement in the main function is not executed, and only the defer in the current Goroutine is executed. Because the runtime.deferproc corresponding to the defer keyword will associate the deferred call function with the Goroutine where the caller is located, so when the program crashes, only the deferred call function of the current Goroutine will be called.
(II) recover Only Takes Effect When Called in defer
The following code reflects this feature:
func main() { defer fmt.Println("in main") if err := recover(); err != nil { fmt.Println(err) } panic("unknown err") }
The running result is:
$ go run main.go
in main
panic: unknown err
goroutine 1 [running]:
main.main()
...
exit status 2
By carefully analyzing this process, it can be known that recover will only take effect when called after a panic occurs. However, in the above control flow, recover is called before panic, which does not meet the conditions for taking effect. Therefore, the recover keyword needs to be used in defer.
(III) panic Allows Multiple Nested Calls in defer
The following code shows how to call panic multiple times in a defer function:
func main() { defer fmt.Println("in main") defer func() { defer func() { panic("panic again and again") }() panic("panic again") }() panic("panic once") }
The running result is as follows:
$ go run main.go
in main
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
...
exit status 2
From the output result of the above program, it can be determined that multiple calls to panic in the program will not affect the normal execution of the defer function. Therefore, it is generally safe to use defer for the finalization work.
III. Data Structure of panic
The panic keyword in the source code of the Go language is represented by the data structure runtime._panic. Every time panic is called, a data structure like the following will be created to store relevant information:
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool pc uintptr sp unsafe.Pointer goexit bool }
- argp: It is a pointer to the parameter when defer is called.
- arg: It is the parameter passed in when panic is called.
- link: It points to the earlier called runtime._panic structure.
- recovered: It indicates whether the current runtime._panic has been recovered by recover.
- aborted: It indicates whether the current panic has been forcibly terminated.
From the link field in the data structure, it can be inferred that the panic function can be called continuously multiple times, and they can form a linked list through the link.
The three fields pc, sp, and goexit in the structure are all introduced to fix the problems brought by runtime.Goexit. runtime.Goexit can only end the Goroutine that calls this function without affecting other Goroutines. However, this function will be cancelled by the panic and recover in defer. The introduction of these three fields is to ensure that this function will definitely take effect.
IV. Principle of Program Crash
The compiler will convert the keyword panic into runtime.gopanic. The execution process of this function includes the following steps:
- Create a new runtime._panic and add it to the front of the _panic linked list of the Goroutine where it is located.
- Continuously obtain runtime._defer from the _defer linked list of the current Goroutine in a loop and call runtime.reflectcall to run the deferred call function.
- Call runtime.fatalpanic to abort the entire program.
func gopanic(e interface{}) { gp := getg() ... var p _panic p.arg = e p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) for { d := gp._defer if d == nil { break } d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) if p.recovered { ... } } fatalpanic(gp._panic) *(*int)(nil) = 0 }
It should be noted that three relatively important parts of code are omitted in the above function:
- The code in the recover branch for restoring the program.
- The code for optimizing the performance of the defer call through inlining.
- The code for fixing the abnormal situation of runtime.Goexit.
In version 1.14, the Go language solved the conflict between recursive panic and recover and runtime.Goexit through the submission of runtime: ensure that Goexit cannot be aborted by a recursive panic/recover.
runtime.fatalpanic implements a program crash that cannot be recovered. Before aborting the program, it will print out all the panic messages and the parameters passed in during the call through runtime.printpanics:
func fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() if startpanic_m() && msgs != nil { atomic.Xadd(&runningPanicDefers, -1) printpanics(msgs) } if dopanic_m(gp, pc, sp) { crash() } exit(2) }
After printing the crash message, it will call runtime.exit to exit the current program and return the error code 2. The normal exit of the program is also implemented through runtime.exit.
V. Principle of Crash Recovery
The compiler will convert the keyword recover into runtime.gorecover:
func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil &&!p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
The implementation of this function is very simple. If the current Goroutine has not called panic, then this function will directly return nil, which is also the reason why the crash recovery will fail when called in a non-defer. Under normal circumstances, it will modify the recovered field of runtime._panic, and the recovery of the program is handled by the runtime.gopanic function:
func gopanic(e interface{}) { ... for { // Execute the deferred call function, which may set p.recovered = true ... pc := d.pc sp := unsafe.Pointer(d.sp) ... if p.recovered { gp._panic = p.link for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { gp.sig = 0 } gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") } } ... }
The above code omits the inlining optimization of defer. It takes out the program counter pc and stack pointer sp from runtime._defer and calls the runtime.recovery function to trigger the scheduling of the Goroutine. Before the scheduling, it will prepare the sp, pc, and the return value of the function:
func recovery(gp *g) { sp := gp.sigcode0 pc := gp.sigcode1 gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 gogo(&gp.sched) }
When the defer keyword is called, the stack pointer sp and program counter pc at the time of the call have already been stored in the runtime._defer structure. The runtime.gogo function here will jump back to the position where the defer keyword was called.
runtime.recovery will set the return value of the function to 1 during the scheduling process. From the comments of runtime.deferproc, it can be found that when the return value of the runtime.deferproc function is 1, the code generated by the compiler will directly jump to before the return of the caller function and execute runtime.deferreturn:
func deferproc(siz int32, fn *funcval) { ... return0() }
After jumping to the runtime.deferreturn function, the program has been recovered from the panic and executes the normal logic, and the runtime.gorecover function can also take out the arg parameter passed in when calling panic from the runtime._panic structure and return it to the caller.
VI. Summary
Analyzing the crash and recovery process of the program is rather tricky, and the code is not particularly easy to understand. Here is a simple summary of the program crash and recovery process:
- The compiler is responsible for the work of converting keywords. It converts panic and recover into runtime.gopanic and runtime.gorecover respectively, converts defer into the runtime.deferproc function, and calls the runtime.deferreturn function at the end of the function that calls defer.
- When encountering the runtime.gopanic method during the running process, it will successively take out the runtime._defer structure from the linked list of the Goroutine and execute it.
- If runtime.gorecover is encountered when calling the deferred execution function, it will mark _panic.recovered as true and return the parameter of the panic.
- After this call ends, runtime.gopanic will take out the program counter pc and stack pointer sp from the runtime._defer structure and call the runtime.recovery function to restore the program.
- runtime.recovery will jump back to runtime.deferproc according to the passed-in pc and sp.
- The code automatically generated by the compiler will find that the return value of runtime.deferproc is not 0. At this time, it will jump back to runtime.deferreturn and restore to the normal execution flow.
- If runtime.gorecover is not encountered, it will traverse all the runtime._defer in turn, and finally call runtime.fatalpanic to abort the program, print the parameters of the panic, and return the error code 2.
The analysis process involves a lot of knowledge at the underlying level of the language, and the source code is also relatively obscure to read. It is full of unconventional control flows, jumping back and forth through the program counter. However, it is still very helpful for understanding the execution flow of the program.
Leapcell: The Next-Gen Serverless Platform for Golang Hosting, Async Tasks, and Redis
Finally, I would like to recommend the most suitable deployment platform: Leapcell
1. Multi-Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ