Defer, Panic, and Recover
Go has three related keywords for control that steps outside the normal
top-to-bottom flow: defer, panic, and recover. The first you'll use daily;
the other two you'll use rarely, and this lesson explains why.
defer: run this when the function returns
A defer statement schedules a function call to run when the surrounding
function returns — no matter how it returns:
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// hello
// world
The idiomatic use is cleanup right next to acquisition, so you can't forget it:
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // runs however the function exits
// ...use f...
LIFO order, arguments evaluated now
Deferred calls run in last-in, first-out order — reverse of how they were
scheduled. And a defer's arguments are evaluated at the defer statement, not
when it finally runs:
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
}
// 2
// 1
// 0
Each i was captured when its defer ran, so you get 2 1 0, not three
3s. LIFO makes cleanup nest correctly: the last thing you opened is the first
thing you close.
panic: stop and unwind
A panic aborts the normal flow and starts unwinding the stack — running
every deferred call on the way up — until the program crashes with a message and
a stack trace. The runtime panics on its own for things like a nil dereference
or an out-of-range index; you can also call panic yourself.
recover: stop the unwinding
recover regains control of a panicking goroutine. It only does anything useful
inside a deferred function: called there, it stops the panic and returns the
value passed to panic (or nil if there wasn't one):
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil // dividing by zero panics
}
func main() {
_, err := safeDivide(1, 0)
fmt.Println(err) // recovered: runtime error: integer divide by zero
}
The deferred closure runs during the unwind, recover catches the panic, and we
turn it into an ordinary error return.
Why Go prefers errors
Notice what the example does: it converts a panic back into an error. That's
the whole philosophy. Go handles expected problems — a missing file, bad input —
by returning an error value the caller checks, not by throwing exceptions.
Reserve panic for truly unrecoverable situations, and recover for a couple
of narrow jobs: stopping a panic from escaping a package boundary (turning it
into an error at the public API), or keeping a long-running server alive when one
request handler blows up. If you find yourself using panic/recover for ordinary
control flow, reach for an error instead — that's the Go way.
Practice
1. Safe Call
IntermediateImplement
safe, which calls the functionfnand shields the caller from any panic it might raise:func safe(fn func()) (err error)- If
fnpanics,saferecovers and returns an error whose message contains the panic value. Build it withfmt.Errorf("recovered: %v", r), whereris the recovered value. - If
fnreturns normally,safereturnsnil.
For example:
safe(func() { panic("boom") }) // error, message contains "boom" safe(func() {}) // nil safe(func() { panic(42) }) // error, message contains "42"Use a named result
errand a deferred function that callsrecover, so it can seterrwhile the panic is unwinding.package main import "fmt" // safe calls fn and, if fn panics, recovers and returns an error whose // message contains the panic value. If fn does not panic, it returns nil. func safe(fn func()) (err error) { // TODO: implement return nil } func main() { err := safe(func() { panic("boom") }) fmt.Println(err) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.
- If