Back to blog

Sunday, August 16, 2026

defer in a Go Loop: Why It's a Memory/Resource Leak Trap

Title card for 'defer in a Go Loop: Why It's a Memory/Resource Leak Trap' with a short code example

defer is tied to the function, not the block or the for iteration. A defer f.Close() inside a loop does not close at the end of that pass. It stacks. Every file, sql.Rows, HTTP body, and lock stays live until the outer function returns. That is how you exhaust file descriptors and hold memory you thought you released.

The failure

This looks like the usual acquire-then-defer pattern. It is not safe in a loop.

package main

import "os"

func readMany(paths []string) error {
    for _, p := range paths {
        f, err := os.Open(p)
        if err != nil {
            return err
        }
        defer f.Close() // runs when readMany returns, not each iteration
        // process f
    }
    return nil
}

Ten thousand paths means ten thousand open files until readMany exits. Same bug with db.Query + defer rows.Close() inside for rows.Next(). Moving defer after the loop is worse: only the last handle is closed, and the name may not even be in scope.

defer mu.Unlock() in a loop that Locks each time is the same shape: you stack unlocks and hold the mutex (or N mutexes) far longer than the critical section.

Why this happens

The spec is blunt: a deferred call runs when the surrounding function returns, return, end of body, or panic unwind. A for body is not a function. Each defer appends to that function's list. LIFO cleanup still happens, but only once, at the end, after every iteration has already opened its resource.

That delay is useful for one file in copyFile. It is a leak when the loop is the acquisition site.

The fix

Give each iteration its own function so defer matches the lifetime you want:

func readMany(paths []string) error {
    for _, p := range paths {
        err := func() error {
            f, err := os.Open(p)
            if err != nil {
                return err
            }
            defer f.Close()
            // process f
            return nil
        }()
        if err != nil {
            return err
        }
    }
    return nil
}

A named helper is the same idea and often clearer. Close without defer at the end of the iteration also works if every return path closes, that is easy to get wrong, which is why the inner function exists.

Do not defer in the loop "just in case of panic" unless that inner function is there. Panic unwind still only runs those stacked defers when the outer function leaves.