1. The Mental Model: The Stack and the "Emergency Drop"
Normally, a program climbs a ladder (the Call Stack). main calls f, f calls g, etc.
- Normal Return: We climb down one rung at a time, finishing our work.
- Panic: We lose our grip at the top. We start falling. We don't execute the rest of our work; we just plummet past every rung on the way down.
However, defer statements are safety hooks we placed on the rungs as we climbed up. Even if we are falling (panicking), we will snag on those hooks and execute that specific code before continuing to fall.
2. Tracing the Example Step-by-Step (on Tour of Go website)
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
Phase 1: The Ascent (Building the Stack)
The program runs normally. It calls f, which calls g(0), which calls g(1)... all the way to g(4).
The stack looks like this (top to bottom):
g(4)(Currently running)g(3)(Waiting, hasdefer print 3)g(2)(Waiting, hasdefer print 2)g(1)(Waiting, hasdefer print 1)g(0)(Waiting, hasdefer print 0)f(Waiting, hasdefer recover)main
Phase 2: The Panic (The Fall begins)
Inside g(4), i is 4. The condition i > 3 is true.
- Action: We print "Panicking!"
- Action:
panic("4")is called.
Crucial Change: The runtime switches the goroutine into "Panic Mode". Normal execution stops. The function g(4) does not return to g(3) normally. It effectively crashes g(4) immediately.
Phase 3: The Unwinding (Hitting the Hooks)
Now, the runtime walks down the stack to see who needs to be notified of the crash.
- Drop to
g(3):- Is there a
defer? Yes. - Execute:
fmt.Println("Defer in g", 3) - Any
recover? No. Keep falling.
- Is there a
- Drop to
g(2):- Is there a
defer? Yes. - Execute:
fmt.Println("Defer in g", 2) - Any
recover? No. Keep falling.
- Is there a
- Drop to
g(1)andg(0):- Same thing. We execute the defers (print 1, then 0) and keep falling.
Phase 4: The Catch (Recover)
The panic has now fallen all the way down to f.
- Drop to
f:- Is there a
defer? Yes (the anonymous function). - Execute: The anonymous function runs.
- Inside the defer: We call
recover().
- Is there a
The Magic of Recover:
When recover() is called, it checks: "Am I currently falling due to a panic?"
- Yes: We grab the panic value (
"4"), return it, and stop the falling. The "Panic Mode" flag is turned off.
Phase 5: Resume
Because recover() stopped the panic, the program does not crash.
However, it cannot go back up to g(4)—that stack frame is destroyed. It resumes from where the recovery happened: inside f.
ffinishes its deferred function.freturns normally tomain.mainprints "Returned normally from f."
Why do we need this?
If you removed recover, the fall would continue past f, past main, and crash the entire Go program (exiting the process).
We need this for "Boundary Protection."
Imagine a Web Server handling 1000 requests.
If one request has bad data that causes an "index out of bounds" panic:
- Without Recover: The entire server crashes. All 999 other requests get disconnected.
- With Recover: You wrap the request handler in a
defer ... recover(). That one request crashes, you catch it, log an error ("Internal Server Error"), and the server keeps running for everyone else.
Summary
- Panic is a fall: It stops normal flow and drops down the stack.
- Defer is the only thing that runs: During a panic, only deferred functions run.
- Recover stops the fall: It catches the panic value and resumes normal execution at the bottom of the fall (where the recover happened), not where the panic started.