How to Use the defer Keyword in Go

The defer keyword in Go schedules a function call to run immediately before the surrounding function returns, ensuring reliable resource cleanup.

The exit door sticky note

You open a file to read a configuration, hit a parsing error halfway through, and return early. The file handle stays open. You acquire a mutex to protect a shared counter, a panic occurs, and the lock never unlocks. The program hangs. In other languages, you fight this with try-finally blocks or RAII wrappers that tie cleanup to object lifetimes. Go takes a different path. The defer keyword attaches a function call to the exit of the current function. That call runs automatically when the function returns, whether the return is normal, early, or caused by a panic. This pattern eliminates the boilerplate of manual cleanup while keeping the code readable. Defer turns cleanup into a side effect of the function signature, not a chore for every return path.

How defer works

defer schedules a function call to execute immediately before the surrounding function returns. It does not run the function right away. Instead, it pushes the call onto a stack associated with the current function invocation. When the function returns, the runtime pops calls from that stack and executes them in Last-In-First-Out order. If you defer three things, the third one runs first. This order is deterministic. It matches how resources are usually acquired and released: you release the last acquired resource first.

The arguments to a deferred function are evaluated at the moment the defer statement is executed, not when the function returns. This evaluation timing captures the current state of variables. If a variable changes value after the defer, the deferred call still sees the original value. This behavior prevents bugs where cleanup logic depends on stale data, but it also requires care when deferring inside loops or with changing variables.

Minimal example

Here's the simplest defer pattern: open a resource, defer the close, do work. The close runs before the function returns, even if an error occurs during processing.

func readFile(path string) ([]byte, error) {
    // Open the file and capture the error immediately
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    // Schedule Close to run before readFile returns, regardless of how the function exits
    // This ensures the file handle is released even if ReadAll fails
    defer f.Close()
    
    // Read the entire file content into memory
    data, err := io.ReadAll(f)
    if err != nil {
        return nil, err
    }
    
    // Return the data and a nil error
    return data, nil
}

The if err != nil { return err } pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. You see the error check right where the error happens. Defer complements this style by keeping the cleanup visible near the resource acquisition, without cluttering every return site.

Defer keeps the happy path clean. The unhappy path handles itself.

Walkthrough: arguments and stack

When the compiler sees defer, it transforms the statement into a runtime operation. It captures the function and its arguments at the exact line where defer appears. The arguments are evaluated once. This evaluation happens before the rest of the function continues. The runtime maintains a list of deferred calls per function invocation. When the function returns, the runtime iterates this list backward and calls each function.

Deferred functions can access named return values and modify them before the function actually returns to the caller. This feature enables error wrapping and logging patterns that run automatically on exit.

func processWithNamedReturns() (result string, err error) {
    // Named returns allow defer to modify the return values before they are sent to the caller
    defer func() {
        // Wrap the error with context if an error occurred
        // This runs before the function returns, modifying the named return value
        if err != nil {
            err = fmt.Errorf("processing failed: %w", err)
        }
    }()
    
    // Simulate work that might fail
    result = "data"
    err = errors.New("simulated failure")
    
    // Return uses the named values, which have been modified by the deferred closure
    return
}

The receiver name is usually one or two letters matching the type: (f *File) Close(), NOT (this *File) or (self *File). This convention keeps method signatures short and readable. Defer works with methods just like functions. You can defer f.Close() on a receiver, and the method runs during cleanup.

LIFO order is deterministic. Trust the stack.

Realistic example: HTTP handler with multiple resources

Real code often defers multiple things. An HTTP handler might defer closing a response body, unlocking a mutex, and logging the request duration. The order of defers matters when resources depend on each other. You usually want to close a file before removing it, or unlock a mutex before closing a connection.

func handleUpload(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
    // Context is always the first parameter by convention, named ctx
    // Check for cancellation before allocating resources
    if ctx.Err() != nil {
        return ctx.Err()
    }
    
    // Parse the multipart form and defer closing to release the request body
    err := r.ParseMultipartForm(10 << 20)
    if err != nil {
        return err
    }
    defer r.Body.Close()
    
    // Create a temporary file for processing
    tmpFile, err := os.CreateTemp("", "upload-*")
    if err != nil {
        return err
    }
    // Defer Close last so it runs first during cleanup, ensuring the file is closed before removal
    // LIFO order means the last deferred call executes first
    defer tmpFile.Close()
    defer os.Remove(tmpFile.Name())
    
    // Process the upload logic
    if err := processFile(ctx, tmpFile); err != nil {
        return err
    }
    
    // Send success response
    w.WriteHeader(http.StatusOK)
    return nil
}

Functions that take a context should respect cancellation and deadlines. The ctx parameter allows the handler to check if the request was cancelled before doing heavy work. Defer does not interact with context directly, but you can defer a function that checks context or logs based on context errors.

gofmt is mandatory. The indentation of defer statements follows standard Go formatting. Don't argue about indentation; let the tool decide. Most editors run gofmt on save.

Defer multiple resources in reverse dependency order. Close before you remove.

Pitfalls and runtime behavior

Defer is simple, but it has traps. Arguments are captured at defer time. If you defer fmt.Println(i) inside a loop, i is captured at the moment of defer. If i changes later, the print still shows the captured value. This causes subtle bugs where deferred output doesn't match expectations.

func badLoop() {
    for i := 0; i < 3; i++ {
        // i is captured by value at the time of defer
        // All deferred calls will print the same value if i changes
        defer fmt.Println(i)
    }
    // Prints 3, 3, 3 because i is 3 when the loop ends and defer executes
}

The fix is to use a closure that captures the current value.

func goodLoop() {
    for i := 0; i < 3; i++ {
        // Capture i in a closure to preserve the current iteration value
        // The closure parameter val receives the value of i at this iteration
        defer func(val int) {
            fmt.Println(val)
        }(i)
    }
    // Prints 0, 2, 1 in LIFO order
}

Deferring inside a loop accumulates deferred calls. If the loop runs 1000 times, 1000 calls wait until the function returns. This can cause memory pressure or delayed cleanup. Use a helper function instead when you need cleanup per iteration.

func processItems(items []Item) error {
    for _, item := range items {
        // Call a helper function that defers its own cleanup
        // This ensures cleanup happens immediately after each item
        if err := processOne(item); err != nil {
            return err
        }
    }
    return nil
}

func processOne(item Item) error {
    // Open resource for this item
    res, err := openResource(item)
    if err != nil {
        return err
    }
    // Defer cleanup for this specific resource
    defer res.Close()
    
    // Process item
    return handle(res)
}

The runtime panics with invalid memory address or nil pointer dereference if you defer a method on a nil receiver. Always check the error before deferring a method call. Deferring f.Close() when f is nil causes a panic at return time, not at defer time.

func safeOpen(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    // f is guaranteed non-nil here because Open returned a non-nil error check
    defer f.Close()
    
    // Use f
    return nil
}

Defer does not stop execution. Code after defer runs immediately. The deferred call waits until the end. This distinction matters when you expect defer to act like a return. It doesn't.

_ (underscore) discards a value intentionally. result, _ := ... says "I considered the second return value and chose to drop it". Use it sparingly with errors. Deferring a function that returns an error and discarding it with _ hides failures. Log the error or propagate it instead.

Arguments are captured at defer time. Closures save you from stale values.

Decision: when to use defer

Use defer when you need to guarantee cleanup runs before a function returns, such as closing files or unlocking mutexes. Use a named return value with defer when you need to modify the return value or wrap an error before it leaves the function. Use a closure with defer when you need to capture the current state of a variable that changes after the defer statement. Use a helper function instead of deferring inside a loop when you need cleanup to happen immediately per iteration. Use explicit cleanup code at every return site when the logic is simple and adding defer obscures the flow. Use recover inside a deferred function when you need to catch a panic and prevent the program from crashing, but only in top-level goroutines or entry points where panics should be handled gracefully.

Defer is for cleanup, not control flow. Keep the exit path simple.

Where to go next