These three keywords are Go’s tools for cleanup and for handling the truly exceptional. They sound scary but they’re simple once we see them in action.
defer
defer schedules a function call to run when the surrounding function exits — no matter how it exits (normal return, or even a panic).
In simple language: “do this later, right before I leave, whatever happens.”
Why do we love it? Cleanup. We open something, immediately defer closing it, and never worry about forgetting — even if the function returns from ten different spots.
func readFile(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close() // guaranteed to run when readFile exits
// ... do a bunch of stuff with f, return wherever we want ...
return nil // f.Close() fires right after this
}
defer runs in LIFO order
If we defer multiple things, they run last-in-first-out — like a stack of plates. The last one we deferred runs first.
func main() {
defer fmt.Println("1") // runs last
defer fmt.Println("2") // runs second
defer fmt.Println("3") // runs first
fmt.Println("start")
}
// Output:
// start
// 3
// 2
// 1
The big gotcha: arguments are evaluated immediately
This trips up everyone. When we defer foo(x), Go evaluates x right now, at the defer line — not later when foo actually runs. The call is delayed, but the arguments are frozen at defer time.
func main() {
i := 0
defer fmt.Println("deferred:", i) // i is captured as 0 RIGHT HERE
i = 99
fmt.Println("now:", i)
}
// Output:
// now: 99
// deferred: 0 <-- not 99! it grabbed i when we deferred
If we actually want the latest value, wrap it in a closure (an inline function) so the read happens at exit time:
func main() {
i := 0
defer func() { fmt.Println("deferred:", i) }() // reads i when it runs
i = 99
}
// Output:
// deferred: 99
The other classic use of defer is unlocking a mutex right after locking it, so we can never forget:
mu.Lock()
defer mu.Unlock() // unlock happens automatically on every exit path
// ... critical section ...
panic
panic stops the normal flow of a function. It unwinds the stack (running any deferred functions on the way up), and if nobody catches it, the whole program crashes and prints a stack trace.
Think of it like pulling the fire alarm. It’s loud, dramatic, and meant for emergencies — not everyday “file not found” stuff.
When should we use it? Only for truly unrecoverable situations — bugs that should never happen, broken invariants, impossible states. For normal failures, we return an error (see the errors note). Don’t use panic for control flow.
func mustGetEnv(key string) string {
v := os.Getenv(key)
if v == "" {
// the app literally cannot run without this — fine to panic
panic("missing required env var: " + key)
}
return v
}
recover
recover stops a panic mid-unwind and lets the program keep running. The catch: it only works inside a deferred function. Anywhere else, it does nothing.
In simple language: recover is the net we hang under the trapeze. It only counts if it’s set up (deferred) before the fall.
The classic real-world use is a server that shouldn’t die just because one request handler panicked:
func safeHandler(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil { // catch the panic, if any
log.Printf("recovered from panic: %v", rec)
http.Error(w, "internal error", 500) // tell the client, stay alive
}
}()
riskyWork(r) // if this panics, the deferred func above catches it
}
Interview soundbite
defer schedules a call to run when the function exits, in LIFO order, with its arguments evaluated immediately at the defer line — perfect for closing files and unlocking mutexes. panic unwinds the stack and is reserved for truly unrecoverable bugs, while recover — which only works inside a deferred function — catches a panic so something like a web server can log it and keep serving other requests instead of crashing.