Right, so you’ve started sprinkling defer statements throughout your code like a responsible adult. You’re cleaning up your files, closing your connections, and unlocking your mutexes. It feels good, doesn’t it? Like you’re finally writing code that won’t leak resources all over the place. But now you’re starting to wonder: “Okay, but when exactly does this cleanup happen? And what if I have more than one?”

Let’s cut to the chase. A defer statement doesn’t just run whenever it feels like it. It runs when the function that contains it returns. Not before, not after. But here’s the critical part you need to burn into your brain: deferred function calls are executed in Last-In-First-Out (LIFO) order. Think of it like a stack of plates. The last plate you put on the stack is the first one you take off.

The language designers chose this LIFO order for a damn good reason: it’s the most logical. Your setup and teardown are usually mirror images. The last resource you acquire is often the first one you need to release. A defer ensures that teardown happens in the exact reverse order of setup, preventing situations where you try to release a fundamental resource (like a network connection) before a dependent resource (like a transaction on that connection) has been cleaned up.

The Stack in Action

Let’s look at this in practice. This isn’t just academic; it’s how you’ll reason about your code.

package main

import "fmt"

func main() {
    fmt.Println("Start of main")
    defer fmt.Println("First defer in main")  // This gets placed on the stack first
    defer fmt.Println("Second defer in main") // This gets placed on the stack second (on top)
    defer fmt.Println("Third defer in main")  // This gets placed on the stack third (on top-top)
    fmt.Println("End of main")
    // Now the function returns. The deferred stack is popped from the top.
}

Run this. The output is unambiguously clear:

Start of main
End of main
Third defer in main
Second defer in main
First defer in main

See? The last defer we declared ("Third defer...") was the first one executed. It’s a stack. This behavior is perfect for resource management.

Why LIFO is Your Best Friend

Imagine you’re opening a file and then reading its content. You want to guarantee the file is closed after any operations on its content are finished. LIFO makes this trivial and intuitive.

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // This will be executed SECOND (pushed first)

    data := make([]byte, 1024)
    _, err = file.Read(data)
    if err != nil {
        return err
    }
    defer fmt.Println("Finished reading file!") // This will be executed FIRST (pushed last)

    fmt.Printf("Data: %s\n", data)
    return nil
    // On return: 1. Print "Finished reading file!", 2. Close the file.
}

The output message runs before the file is closed, which is fine because we’re done with the data. The crucial part is that file.Close() is deferred before the read operation, so it’s guaranteed to run after it. This mirroring is why LIFO is the only sane choice.

The Pitfall: Defer and Loop

Here’s where people get tripped up, and it’s a doozy. What happens if you put a defer inside a loop?

func processFiles(filenames []string) {
    for _, name := range filenames {
        file, err := os.Open(name)
        if err != nil {
            log.Println(err)
            continue
        }
        defer file.Close() // DANGER! DANGER!

        // Process the file...
    }
    // All defers from every successful loop iteration happen HERE.
}

This code is a resource disaster waiting to happen. The defer calls don’t execute until the processFiles function returns. If you’re processing 10,000 files, you’re stacking 10,000 defer calls, and all those files will remain open until the very end of the function. You will run out of file descriptors. This is the most common “I don’t understand defer” mistake.

The solution? Don’t use defer in a loop unless you’re absolutely sure the number of iterations is small and manageable. Instead, close the resource explicitly inside the loop right after you’re done with it.

func processFilesCorrectly(filenames []string) {
    for _, name := range filenames {
        func() { // An anonymous function to scope the defer
            file, err := os.Open(name)
            if err != nil {
                log.Println(err)
                return
            }
            defer file.Close() // This defers until the anonymous function returns!

            // Process the file...
        }() // The anonymous function ends here, the defer runs, and the file is closed.
    }
}

Wrapping the loop’s inner work in an anonymous function is the classic trick. The defer is scoped to that small function, so it runs at the end of each iteration, not at the end of the entire parent function. Problem solved. Remember this. It will save you from a world of pain.