Explain defer — how does it work, and what are common pitfalls?

5 minintermediategodefererror-handlingfundamentals

Quick Answer

defer schedules a function call to run after the enclosing function returns, in LIFO order if multiple defers are registered. It's most commonly used for cleanup, like closing a file or unlocking a mutex, guaranteeing that cleanup runs even if the function returns early or panics. Two common pitfalls: defer arguments are evaluated immediately, when the defer statement runs, not when the deferred call actually executes — so defer fmt.Println(x) captures x's value at that point, not its final value; and deferring inside a loop accumulates all deferred calls until the enclosing function returns, not the loop iteration, which can exhaust resources like file handles if you're opening many files in a loop.

Detailed Answer

defer is Go's answer to finally blocks, but its exact evaluation timing surprises newcomers.

Basic usage

func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()   // runs when readFile returns, no matter which path

    // ... use f ...
    return nil
}

LIFO ordering

func demo() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
}
// Output: 3
//         2
//         1

Pitfall: arguments evaluated immediately

func demo() {
    x := 1
    defer fmt.Println("x was:", x)  // "x" captured as 1 right now
    x = 2
}
// Output: "x was: 1", not 2

If you need the final value, defer a closure instead:

defer func() { fmt.Println("x is now:", x) }()  // reads x at call time

Pitfall: defer in a loop

func processFiles(paths []string) error {
    for _, p := range paths {
        f, err := os.Open(p)
        if err != nil {
            return err
        }
        defer f.Close()   // BUG: all files stay open until processFiles returns
    }
    return nil
}

With hundreds of files, this can exhaust the OS's file descriptor limit before processFiles ever returns. Fix it by wrapping the loop body in its own function, so each defer fires per iteration:

func processOne(p string) error {
    f, err := os.Open(p)
    if err != nil {
        return err
    }
    defer f.Close()
    // ... use f ...
    return nil
}

Related Resources