skip to content

What does Go's `defer` statement do, and when does the deferred call actually run?

level: juniorimportance: must knowfreq 85%

answer

  1. not where it is written
  2. the function, not the block
  3. cleanup mirrors acquisition
  4. newest registered runs first
  5. os.Exit walks past all of them

basics

~10 s

defer postpones a function call until the surrounding function finishes, not until the enclosing block ends. Several deferred calls run in reverse registration order, last registered first, and they run on every return path.

solid answer

~40 s

`defer` attaches a call to the *currently executing function*, not to the block it is written in. The call is registered where the `defer` statement executes and runs when that function is on its way out — after the return value is set, just before control goes back to the caller. If a function registers several deferred calls they run last-in-first-out, so cleanup unwinds in the reverse order of acquisition: `defer f.Close()` after opening, `defer mu.Unlock()` after locking. Deferred calls run on every exit path — a normal return, an early `return` in a guard clause, or while the goroutine is unwinding — which is exactly why they are the idiomatic place for cleanup. The one thing that skips them is `os.Exit`, which terminates the process immediately.

code

go · 7 lines
go
func demo(path string) {
	defer fmt.Print("1")
	if path != "" {
		defer fmt.Print("2") // not at the closing brace of the if
	}
	fmt.Print("body ")
}

go deeper

for a junior

Be ready to state the rule cleanly: the call is postponed until the surrounding function returns, and several of them run newest-first. Show the open-then-defer-Close pattern from memory.

for a middle

Explain why the scope is the function rather than the block, and why last-in-first-out is the order that makes cleanup safe. Know that a normal return, an early return and an unwinding goroutine all run defers while os.Exit does not.

for a senior

Demonstrate the judgment: use defer so no future early return can skip cleanup, keep os.Exit and log.Fatal out of functions that hold resources, and recognise on review when a defer registered in a loop will not fire soon enough.

for a principal

Own the convention for the codebase: where the process is allowed to exit, whether cleanup is guaranteed on every shutdown path, and how much a team should rely on deferred cleanup versus explicit lifecycle ownership in long-lived services.

## What `defer` actually is `defer` takes a **call expression** and postpones it. The line `defer f.Close()` does not close the file where it is written; it registers that call against the function currently running, and the call is executed when that function finishes. It must be a call, not a function name: `defer f.Close()` compiles, `defer f.Close` does not. ## The unit is the function, not the block This is the single most common misreading. Go's `defer` is **function-scoped**. A `defer` inside an `if`, a `for`, or a bare `{ }` block does not fire when that block ends — it waits for the enclosing function to return. ```go func demo(path string) { defer fmt.Print("1") if path != "" { defer fmt.Print("2") // NOT at the closing brace of the if } fmt.Print("body ") } ``` `demo("x")` prints `body 21`. Both deferred calls wait until `demo` returns. Engineers arriving from languages where a scope-exit hook is block-scoped (C++ destructors, Python's `with`, Java's try-with-resources) trip on this constantly, and it is the direct cause of the classic "defer inside a loop keeps every file open" bug. ## Ordering: last-in-first-out Deferred calls run in the **reverse of the order they were registered**. The function keeps them in a stack; the last one registered is the first one executed. In the example above `"2"` was registered second, so it prints first. LIFO is not an arbitrary choice — it makes cleanup mirror acquisition. If you open a file, then lock a mutex protecting the index, then start a timer, the reverse order tears them down in the only order that is safe: nothing is released while something registered later still depends on it. Written in the idiomatic style, acquisition and release sit adjacent: ```go f, err := os.Open(path) if err != nil { return err } defer f.Close() mu.Lock() defer mu.Unlock() ``` A reviewer can see, on one line each, that every acquired resource is released. ## Which exit paths run defers Deferred calls run: - on a normal fall-off-the-end return; - on an explicit `return`, including an early return from a guard clause deep in the function — this is why `defer` is more reliable than a cleanup block at the bottom, which a new `return` silently skips; - while a goroutine is unwinding after a panic, which is what makes deferred cleanup survive a crash on that path. They do **not** run when the process is terminated outright. `os.Exit` exits immediately without touching pending deferred calls, and anything built on it inherits that behaviour — `log.Fatal` and friends call `os.Exit`, so a `defer f.Close()` above a `log.Fatalf` never runs. If you need cleanup, return an error up to a small `main` that decides the exit code, rather than exiting from deep inside the call tree. A deferred call also belongs to **one function in one goroutine**. Starting a goroutine does not carry the caller's deferred calls into it, and a goroutine that never finishes never runs the calls its own function deferred. ## The order of events at return When a function returns, three things happen in sequence: the return values are set, then the deferred calls run (newest first), then control returns to the caller. The middle step is genuinely *after* the result has been assigned, which is why a deferred closure can still adjust a **named** result before the caller sees it. ## Everyday uses Closing a file or an HTTP response body, unlocking a mutex, stopping a `time.Ticker`, restoring a global or a config value you temporarily changed in a test, and marking a span of work finished. The common thread: something you must undo exactly once, on every path out, written next to the thing that made it necessary. ## Two caveats that bite First, the **arguments** of a deferred call are evaluated at the `defer` statement, not at return — so `defer fmt.Println(x)` prints the `x` of that moment. Second, because `defer` is function-scoped, registering one per iteration of a long loop accumulates work and holds every resource open until the whole function ends; per-item cleanup needs a per-item function call.

  • Does a deferred call still run if the function calls os.Exit(1)?
    No. `os.Exit` terminates the process immediately and pending deferred calls are skipped, so a `defer f.Close()` above it never runs. `log.Fatal` and `log.Fatalf` call `os.Exit` too, so they behave the same way. The fix is to return an error up to a small `main` that prints and exits, keeping the exit call out of the code that owns resources.
  • Why does Go run deferred calls in reverse order rather than the order they were written?
    Because cleanup has to unwind acquisition. Later work usually depends on earlier work — you lock after you open, you write after you lock — so releasing newest-first guarantees nothing is torn down while something registered after it still needs it. It also means each `defer` can sit immediately under the statement it undoes and still compose correctly with the ones around it.
  • Is `defer f.Close` (without parentheses) valid Go?
    No, it is a compile error. `defer` requires a call expression, so the compiler rejects a bare function value. Write `defer f.Close()`, or `defer func() { ... }()` when you want a block of cleanup. The parentheses are also what makes the argument-evaluation rule meaningful: the call's arguments are evaluated right there, and only the invocation is postponed.

Deferred calls are notes you pin to the door on your way in; you read them only as you leave the room, starting with the one pinned most recently.

saying these in an interview costs you the question

  • Says the deferred call runs when the enclosing block ends
  • Claims deferred calls execute in the order they were written
  • Thinks an early return skips pending deferred calls
  • Expects os.Exit or log.Fatal to run pending defers
  • Believes defer only works with function literals