Learn Go
intermediate1 min read

Defer, Panic, and Recover

Go provides three built-in mechanisms for controlling unusual execution flow: defer for cleanup, panic for unrecoverable situations, and recover for catching panics at a boundary.

defer

defer schedules a function call to run just before the surrounding function returns. Deferred calls execute even if the function panics:

package main
 
import "fmt"
 
func greet(name string) {
    defer fmt.Println("goodbye,", name)
    fmt.Println("hello,", name)
}
 
func main() {
    greet("Alice")
    // Output:
    // hello, Alice
    // goodbye, Alice
}

Typical Use: Resource Cleanup

The idiom is to open a resource and immediately defer its close:

package main
 
import (
    "fmt"
    "os"
)
 
func readFile(path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", err
    }
    defer f.Close() // always runs when readFile returns
 
    buf := make([]byte, 256)
    n, _ := f.Read(buf)
    return string(buf[:n]), nil
}
 
func main() {
    content, err := readFile("/etc/hostname")
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Print(content)
}

LIFO Ordering

Multiple deferred calls execute in last-in, first-out order:

package main
 
import "fmt"
 
func main() {
    defer fmt.Println("third")
    defer fmt.Println("second")
    defer fmt.Println("first")
    fmt.Println("start")
    // Output:
    // start
    // first
    // second
    // third
}

Deferred Function Arguments Are Evaluated Immediately

The arguments to a deferred function are evaluated when the defer statement is reached, not when the deferred function runs:

package main
 
import "fmt"
 
func main() {
    x := 10
    defer fmt.Println("deferred x:", x) // captures x=10 now
 
    x = 20
    fmt.Println("current x:", x)
    // Output:
    // current x: 20
    // deferred x: 10
}

panic

panic stops normal execution and begins unwinding the call stack. All deferred functions run during unwinding before the program terminates:

package main
 
import "fmt"
 
func mustPositive(n int) int {
    if n <= 0 {
        panic(fmt.Sprintf("expected positive, got %d", n))
    }
    return n
}
 
func main() {
    fmt.Println(mustPositive(5)) // 5
    fmt.Println(mustPositive(-1)) // panics
}

Idiomatic Go: Reserve panic for truly unrecoverable programmer errors (e.g., invalid arguments to a library function). Return error values for expected failure conditions.

recover

recover stops a panic and returns the value passed to panic. It only has an effect when called directly from a deferred function:

package main
 
import "fmt"
 
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
 
    result = a / b // panics if b == 0
    return
}
 
func main() {
    r, err := safeDiv(10, 2)
    fmt.Println(r, err) // 5 <nil>
 
    r, err = safeDiv(10, 0)
    fmt.Println(r, err) // 0 recovered: runtime error: integer divide by zero
}

Middleware Pattern

A common use of recover in web servers is a top-level panic handler that prevents one request from crashing the whole server:

package main
 
import (
    "fmt"
    "net/http"
)
 
func recoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                http.Error(w, "internal server error", http.StatusInternalServerError)
                fmt.Println("recovered panic:", rec)
            }
        }()
        next(w, r)
    }
}
 
func handler(w http.ResponseWriter, r *http.Request) {
    panic("something went very wrong")
}
 
func main() {
    http.HandleFunc("/", recoveryMiddleware(handler))
    fmt.Println("listening on :8080")
    http.ListenAndServe(":8080", nil)
}

Key Takeaways