The defer Statement
Learn how the defer keyword delays function execution until the surrounding function returns, its LIFO ordering, argument evaluation timing, and common cleanup patterns.
The defer statement schedules a function call to run after the function that contains it finishes, no matter how that function exits — a normal return, a runtime panic, or even a fall‑through from the last line. It is the primary mechanism in Go for pairing resource acquisition with guaranteed release, and it removes an entire class of clean‑up bugs that come from forgetting to close something before every return path.
Think of defer as a sticky note you attach to an action: “do this, then at the end of the room, tidy up in reverse order.” The note waits until you walk out — whether you leave calmly or in a panic — and only then does the cleaning happen.
What defer Does
When the compiler encounters a defer statement, it does not execute the deferred function immediately. Instead, it records the function call on a per‑goroutine list. At the moment the surrounding function returns, the recorded calls are executed in last‑in‑first‑out (LIFO) order. If the function panics, deferred calls still run during the stack unwind, which is exactly what makes recover possible.
A minimal example shows the behaviour clearly:
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("deferred")
fmt.Println("end")
}
This prints:
start
end
deferred
The call to fmt.Println("deferred") runs only after main is about to return, after the last explicit statement has finished. You do not need to place the cleanup logic at every exit point; the deferred call covers them all.
How Deferred Calls Are Ordered
Deferred function calls form a stack — the most recently deferred call runs first. This LIFO discipline is natural for nested resources: if you open A, then inside that open B, you must close B before closing A.
func demo() {
defer fmt.Println("first deferred")
defer fmt.Println("second deferred")
defer fmt.Println("third deferred")
fmt.Println("body")
}
The output is:
body
third deferred
second deferred
first deferred
LIFO matches real resource ownership:
When you acquire multiple resources sequentially, deferring each close right after its open automatically produces the correct reverse order — the last resource opened is the first one closed. You never need to manually track a cleanup order.
The stack metaphor is not just a curiosity; it guarantees that nested operations unwind cleanly. For instance, opening a database transaction, then a statement, and then executing a query: deferring the rollback, then the statement close, then the commit in reverse order respects the dependency chain.
Argument Evaluation at the Defer Point
One of the most misunderstood aspects of defer is that the arguments to the deferred function are evaluated at the moment the defer statement executes, not when the deferred function finally runs. The function itself is called later, but the values it receives are frozen at the point of deferral.
func evaluateExample() {
i := 0
defer fmt.Println("deferred i:", i)
i++
fmt.Println("final i:", i)
}
This prints:
final i: 1
deferred i: 0
Even though i becomes 1 before the function returns, the deferred Println captured the value 0 because that was the value of i when defer was encountered.
If the argument is a pointer, the pointer itself is captured early, but the data it points to can change before the deferred call executes.
func pointerExample() {
i := 0
defer fmt.Println("deferred *p:", *(&i))
i++
fmt.Println("final i:", i)
}
Here the address of i is evaluated immediately, and dereferencing it at defer execution sees the modified value, so the deferred print shows 1. This is why deferred functions that close over variables via closures behave differently from passing values directly.
Closures vs. arguments:
A common pitfall: defer func() { fmt.Println(i) }() does not capture a copy of i; it reads the variable at execution time. If i changes, the deferred call sees the final value. If you need the value at defer time, pass it as an explicit argument.
Defer and Named Return Values
A deferred function can read and assign to the named return values of the enclosing function. Because deferred calls run after the return statement has determined the return value but before the caller receives it, a deferred function can modify what the caller sees.
func modifyReturn() (result int) {
defer func() {
result *= 2
}()
return 5
}
This function returns 10. The return 5 statement assigns 5 to result, then the deferred closure doubles result, and finally the caller gets the updated value. This pattern is used for cleaning up and adjusting the error return after a function body has finished.
A common production use is wrapping an error with more context:
func copyFile(dst, src string) (err error) {
f, openErr := os.Open(src)
if openErr != nil {
return openErr
}
defer f.Close()
d, createErr := os.Create(dst)
if createErr != nil {
return fmt.Errorf("create dst: %w", createErr)
}
defer func() {
if closeErr := d.Close(); closeErr != nil && err == nil {
err = closeErr
}
}()
_, err = io.Copy(d, f)
return err
}
The second deferred closure inspects the error from closing the destination file. If io.Copy succeeded but the final close fails, the function returns that close error instead of nil. Without named return values, this kind of after‑the‑fact error injection would be much clumsier.
Only named returns can be modified:
If the function signature uses anonymous return values (func f() int), a deferred function cannot alter what the caller receives, because there is no named variable to reach. The return value is already copied to the caller's frame by the time deferred functions run.
Practical Cleanup Patterns
The primary reason defer exists is to make cleanup code both obvious and safe. The following patterns appear in nearly every Go codebase.
Closing Resources
Resources like files, network connections, and HTTP response bodies must be closed to avoid leaks. The idiomatic Go pattern acquires the resource, checks the error, and immediately defers the close.
resp, err := http.Get("https://example.com")
if err != nil {
return err
}
defer resp.Body.Close()
This single line guarantees that no matter how many later return statements appear, the body is always closed.
Close a resource only after nil‑checking the error:
resp.Body is only valid if http.Get returned a non‑nil response. Calling defer resp.Body.Close() before checking err will panic with a nil pointer dereference. Always defer after the error check.
Unlocking Mutexes
var mu sync.Mutex
func updateState() {
mu.Lock()
defer mu.Unlock()
// critical section
}
The lock is released exactly once when the function returns, even if the critical section contains early returns or panics. Without defer, every return path must remember to call mu.Unlock().
Database Transactions and Statements
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no-op after Commit
stmt, err := tx.Prepare("INSERT ...")
if err != nil {
return err
}
defer stmt.Close()
// ... use stmt ...
return tx.Commit()
The Rollback deferred call is harmless after a successful Commit (committed transactions ignore rollback), so the function remains safe regardless of which path it takes.
Releasing Other Finite Resources
The pattern generalises to any finite resource: semaphores, worker pool slots, file locks, and even custom cleanup callbacks. Deferring a release right after acquisition keeps the acquire‑release pair visually coupled and mechanically safe.
Defer in a Loop — Accumulating Calls
A defer inside a loop body does not run until the enclosing function returns, not when the loop iteration ends. This can lead to a massive buildup of deferred calls if the loop runs many times.
func readManyFiles(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // all files stay open until function exits
// process f
}
return nil
}
Every opened file remains open until readManyFiles returns, potentially exhausting file descriptors. The correct approach is to wrap each iteration in an anonymous function so that defer applies to that smaller scope:
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
}
}
Now f.Close() runs at the end of each iteration, not at the end of readManyFiles.
Loops and deferred resource leaks:
A deferred call in a loop body without an enclosing function scope is one of the most common memory and resource leaks in Go. Always ask: “Will this defer inside a loop stack up calls across hundreds or thousands of iterations?” If the answer is yes, wrap the loop body in a helper function or an immediately invoked closure.
Defer and Error Handling
Because deferred calls always run, they execute even on error paths. This is exactly what you want for releasing resources, but it can cause confusion when the cleanup itself depends on whether an error occurred.
A common situation: you open a source file and a destination file. If creating the destination fails, you must close the source before returning. With defer, the code becomes straightforward:
func copyFile(dst, src string) (err error) {
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
d, err := os.Create(dst)
if err != nil {
return err
}
defer d.Close()
_, err = io.Copy(d, s)
return err
}
Even if os.Create fails, the deferred s.Close() runs and cleans up the already‑opened source file. Without defer, you would need an explicit s.Close() before that second return.
When the cleanup function itself can fail, deferring it inside a named‑return closure (as shown earlier) lets you capture that error without discarding the original one, as long as no error had already occurred. That nuanced error wrapping is idiomatic for functions that manage multiple resources.
Common Mistakes and Gotchas
Deferred nil function calls
If the deferred function is stored in a variable that is nil at the time of deferral, the program will panic when the function actually runs, not when defer is written.
var f func()
defer f() // defers a nil call
// later: runtime panic
Always ensure the deferred function value is non‑nil.
os.Exit does not run deferred functions
os.Exit immediately terminates the program; deferred functions are never executed. If you need finalisation logic, call it explicitly before os.Exit, or structure the program so that main returns naturally.
Relying on deferred execution inside goroutines
A deferred function runs when the goroutine’s entry function returns, not when the program ends. If the main goroutine exits, still‑running goroutines are killed without running their deferred calls. Use synchronisation primitives to let goroutines finish.
Argument evaluation surprises with loop variables
If you defer a function that captures a loop variable by value, each deferred call gets the same value — the one that was current when the defer ran, not the value from the iteration you might expect. Using an explicit parameter to pass the loop variable freezes the correct value.
for _, v := range values {
defer fmt.Println(v) // v evaluated at each iteration, correct
}
This works because v is evaluated immediately in the argument list. The closure variant defer func() { fmt.Println(v) }() would print the final value of v for all calls, which is a classic bug.
Closures over loop variables are a known trap:
Go 1.22 changed loop variable semantics so that each iteration gets a new variable, reducing this surprise for new code. However, understanding the underlying mechanics is essential when reading older Go or when the loop variable is captured by a pointer.
Blocking operations inside a deferred function
A deferred close() on a channel, or a network call inside a defer, can cause delays when the function returns. While this is sometimes acceptable, be aware that deferred functions contribute to the total exit time of the function and, in the case of panics, to the unwind latency.
Defer and the Panic/Recover Mechanism
Deferred functions are the only place where recover has any effect. When a panic occurs, the runtime immediately starts unwinding the goroutine’s stack, running all deferred functions in LIFO order. If one of those deferred functions calls recover, the panic stops, and execution resumes normally after the deferred call that recovered.
This interaction is covered in detail in the following sections on panic and recover. For now, the essential point is: defer is the mechanism that keeps your cleanup running even when the world is falling apart, and it is the only hook that lets you catch a panic.
Summary
defer takes the cleanup code that would normally be scattered across multiple return paths and hoists it right next to the resource acquisition. That single mechanical shift eliminates an entire class of resource leak bugs and makes functions easier to read because the “close” sits directly beside the “open”.
The three mechanical rules you must internalise are:
- Arguments to the deferred function are evaluated when the
deferstatement runs. - Deferred calls execute in last‑in‑first‑out order after the function body finishes.
- A deferred function can modify the caller‑visible result only if the return values are named.