defer

The defer statement schedules a function call to run just before the surrounding function returns, no matter how it returns — normally, via an early return, or while a panic is unwinding the stack. It is Go’s primary tool for guaranteeing cleanup: closing files, unlocking mutexes, closing network connections, or logging that a function finished. Because defer is a language keyword rather than a library convention, the compiler and runtime guarantee the deferred call always runs, which makes resource management in Go far less error-prone than manually placing cleanup code at every possible exit point.

Overview / How It Works

Languages like Java or Python use try/finally to guarantee cleanup runs regardless of how a block exits. Go has no exceptions and no try/finally; instead it has defer, which attaches directly to the function whose resources need cleaning up, right next to the line that acquired them. This keeps the acquisition and the cleanup visually next to each other in the source, instead of separated into a finally block far below.

Every goroutine has its own defer stack. When execution reaches a defer statement, Go immediately evaluates the function (or method) value and all of its arguments, and pushes that fully-resolved call onto the current function’s defer stack. The call itself is not executed yet — only scheduled. Execution then continues to the next statement as normal.

When the surrounding function is ready to return — whether it reaches the end of its body, hits an explicit return, or is unwinding because of a panic — Go pops the defer stack and runs each deferred call, one at a time, in last-in, first-out (LIFO) order. The most recently deferred call runs first. Only once every deferred call has finished does control actually pass back to the caller. This is also why recover(), which stops a panic from continuing to unwind, only works when it is called directly inside a deferred function: that is the only place still running “inside” the panicking function by the time the panic reaches it.

Modern Go (the compiler since Go 1.14, further improved since) recognizes many common defer patterns — a fixed, small number of unconditional defers in a function — and compiles them as “open-coded defers” that avoid the overhead of a real stack push, so defer is cheap in the vast majority of real code. You should reach for it freely for correctness; only avoid it in extremely hot, tight loops where you can just call the cleanup inline instead.

Syntax

defer functionCall(arg1, arg2)
Part Meaning
defer Keyword that schedules the following call to run when the enclosing function returns.
functionCall Any function value: a plain function, a method (obj.Method()), or a function literal (func() { ... }()).
(arg1, arg2) Arguments are evaluated immediately, at the moment the defer statement runs — not when the call later executes.

You can defer a plain function call, a method call, or an anonymous function literal that you invoke immediately (a closure). The closure form is common when you need to reference variables by name at the time the deferred code actually runs, rather than capturing their value up front.

Examples

Example 1: Basic ordering

package main

import "fmt"

func main() {
	fmt.Println("start")
	defer fmt.Println("deferred call")
	fmt.Println("end")
}

Output:

start
end
deferred call

The deferred fmt.Println call is registered when execution passes the defer line, but it does not run then. It runs only after every other statement in main has executed — here, after "end" is printed and just before main actually returns.

Example 2: LIFO order with multiple defers

package main

import "fmt"

func main() {
	for i := 1; i <= 3; i++ {
		defer fmt.Println("deferred:", i)
	}
	fmt.Println("main function body finished")
}

Output:

main function body finished
deferred: 3
deferred: 2
deferred: 1

Each loop iteration schedules a new deferred call, and each one captures the current value of i immediately (because arguments to a deferred call are evaluated right away). When main finishes, the three deferred calls run in reverse order of how they were scheduled — 3, then 2, then 1 — because the defer stack is LIFO.

Example 3: Resource cleanup with an early return

package main

import (
	"errors"
	"fmt"
)

type Resource struct {
	name string
}

func (r *Resource) Close() {
	fmt.Println("closing resource:", r.name)
}

func process(fail bool) error {
	r := &Resource{name: "db-connection"}
	defer r.Close()

	fmt.Println("using resource:", r.name)

	if fail {
		return errors.New("something went wrong")
	}

	fmt.Println("processing succeeded")
	return nil
}

func main() {
	if err := process(false); err != nil {
		fmt.Println("error:", err)
	}

	fmt.Println("---")

	if err := process(true); err != nil {
		fmt.Println("error:", err)
	}
}

Output:

using resource: db-connection
processing succeeded
closing resource: db-connection
---
using resource: db-connection
closing resource: db-connection
error: something went wrong

This is the pattern defer is built for: r.Close() is scheduled once, right after the resource is acquired, and it runs whether process returns normally (the first call) or exits early through the if fail branch (the second call). There is no way to reach the end of process without Close running, even though the function has two different exit points.

How It Works Step by Step

  • Go reaches a defer statement and immediately evaluates the function value and its arguments, then pushes the resolved call onto the current goroutine's defer stack.
  • Execution continues normally to the next statement; the deferred call has not run yet.
  • When the function is about to return — normal fall-through, explicit return, or a panic unwinding the stack — Go pops the defer stack and runs each deferred call in LIFO order.
  • If a deferred function calls recover() while the goroutine is panicking, the panic is stopped there and the function returns normally instead of crashing the program.
  • Only after all deferred calls finish does control return to the caller, which is why a deferred closure can still read — and even modify — the function's named return value before the caller sees it.

The next two points are easy to get wrong, so they deserve their own examples.

Arguments are evaluated immediately, not when the call runs

package main

import "fmt"

func main() {
	x := 10
	defer fmt.Println("deferred x:", x)
	x = 20
	fmt.Println("final x:", x)
}

Output:

final x: 20
deferred x: 10

Even though x is 20 by the time the deferred call actually executes, it prints 10, because x's value was copied into the deferred call's arguments at the moment the defer statement ran — before x was reassigned.

A deferred closure can modify a named return value

package main

import "fmt"

func increment() (result int) {
	defer func() {
		result++
	}()
	return 5
}

func main() {
	fmt.Println(increment())
}

Output:

6

return 5 sets the named return value result to 5, but the function has not actually returned to its caller yet — the deferred closure still runs first, increments result to 6, and only then does increment hand 6 back to main. This technique is exactly how many logging or error-wrapping helpers, and recover-based error converters, adjust a function's return value from within a defer.

Common Mistakes

Mistake 1: Deferring inside a loop instead of a helper function

package main

import "fmt"

type File struct {
	name string
}

func openFile(name string) *File {
	fmt.Println("opened:", name)
	return &File{name: name}
}

func (f *File) Close() {
	fmt.Println("closed:", f.name)
}

func processAll(names []string) {
	for _, name := range names {
		f := openFile(name)
		defer f.Close()
		fmt.Println("processing:", f.name)
	}
	fmt.Println("all files processed")
}

func main() {
	processAll([]string{"a.txt", "b.txt", "c.txt"})
}

Output:

opened: a.txt
processing: a.txt
opened: b.txt
processing: b.txt
opened: c.txt
processing: c.txt
all files processed
closed: c.txt
closed: b.txt
closed: a.txt

This compiles fine and is a very common mistake: defer only runs when the enclosing function returns, not when the loop iteration ends. If processAll looped over thousands of files, every one of them would stay open simultaneously until the whole loop finished, which can exhaust the operating system's file-descriptor limit. The fix is to move the loop body into its own function, so each call gets its own stack frame and its own defer runs immediately when that call returns:

package main

import "fmt"

type File struct {
	name string
}

func openFile(name string) *File {
	fmt.Println("opened:", name)
	return &File{name: name}
}

func (f *File) Close() {
	fmt.Println("closed:", f.name)
}

func processOne(name string) {
	f := openFile(name)
	defer f.Close()
	fmt.Println("processing:", f.name)
}

func processAll(names []string) {
	for _, name := range names {
		processOne(name)
	}
	fmt.Println("all files processed")
}

func main() {
	processAll([]string{"a.txt", "b.txt", "c.txt"})
}

Output:

opened: a.txt
processing: a.txt
closed: a.txt
opened: b.txt
processing: b.txt
closed: b.txt
opened: c.txt
processing: c.txt
closed: c.txt
all files processed

Now each file closes right after it's used instead of piling up.

Mistake 2: Expecting deferred arguments to see later changes

package main

import "fmt"

func main() {
	x := 1
	defer fmt.Println("x at end:", x)
	x = 2
	x = 3
	fmt.Println("current x:", x)
}

Output:

current x: 3
x at end: 1

Beginners often expect the deferred line to print the final value of x (3), but it prints 1, because x was copied into the call's arguments the instant the defer statement executed. If you actually want the deferred code to see the variable's value at the time it runs, wrap it in a closure so it reads the variable by reference instead of copying it up front:

package main

import "fmt"

func main() {
	x := 1
	defer func() {
		fmt.Println("x at end:", x)
	}()
	x = 2
	x = 3
	fmt.Println("current x:", x)
}

Output:

current x: 3
x at end: 3

The closure captures the variable x itself, not its value at defer-time, so it sees whatever x holds when the closure finally runs.

Mistake 3: Calling recover() outside a deferred function

func safeCall() {
	if r := recover(); r != nil {
		fmt.Println("recovered:", r)
	}
	mayPanic()
}

recover() only has an effect when it is called directly inside a deferred function, while that goroutine is actively panicking. Here it is called as an ordinary statement, before any panic has even happened, so it always returns nil and does nothing — the subsequent call to mayPanic() will crash the program. The fix is to call recover() from inside a function that is itself deferred:

package main

import "fmt"

func mayPanic() {
	panic("something failed")
}

func safeCall() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered:", r)
		}
	}()
	mayPanic()
}

func main() {
	safeCall()
	fmt.Println("program continues")
}

Output:

recovered: something failed
program continues

Now mayPanic panics, the panic starts unwinding safeCall, and the deferred closure runs and calls recover() directly — which stops the panic, lets safeCall return normally, and allows main to keep going.

Best Practices

  • Place the defer immediately after the line that acquires the resource (open the file, then defer f.Close() on the very next line), so cleanup is impossible to forget as the function grows.
  • Keep the deferred call itself simple — a single method call like defer f.Close(). If cleanup needs several steps, wrap them in a small named function or closure rather than crowding the defer line.
  • Never defer inside a loop that doesn't return promptly; extract the loop body into its own function so each iteration's defer fires right away instead of accumulating until the outer function returns.
  • If a deferred cleanup call can itself fail (for example, Close() returning an error), capture that error with a named return value and a closure instead of silently discarding it.
  • Only use recover() inside a deferred function, and only to convert a specific, expected panic into a normal error — it is not a general substitute for error handling.
  • Remember defer stacks are per function call and run LIFO; when several defers in one function depend on ordering (like unlocking a mutex before closing the resource it guards), defer them in the order that produces the correct reverse sequence.
  • Don't over-defer in extremely hot, tight loops where the same cleanup can be written inline — defer is cheap in ordinary code, but inline calls still avoid any scheduling overhead in the rare hot path.

Practice Exercises

  • Write a function withLock(mu *sync.Mutex, work func()) that locks mu, defers unlocking it, and then calls work. Call it several times in a row from main and confirm the program doesn't deadlock.
  • Without running it, predict the exact output of a main function that defers three different fmt.Println calls with three different literal arguments, interspersed with ordinary fmt.Println calls, then check yourself by compiling and running it.
  • Take the "resource leak" example from Common Mistakes, but make processAll loop over 100 names instead of 3. Explain, in a sentence, what real-world problem this could cause with actual OS file handles, and rewrite it using the per-iteration helper-function fix.

Summary

  • defer schedules a function call to run just before its surrounding function returns, whether that return is normal, early, or caused by a panic.
  • The deferred function's arguments are evaluated immediately, at the defer statement itself — only the call is postponed.
  • Multiple defers in the same function run in LIFO (last-in, first-out) order.
  • Deferring inside a loop delays cleanup until the whole function returns, not each iteration — extract the loop body into a helper function to clean up per iteration.
  • A deferred closure can read and modify a named return value before the caller actually receives it.
  • recover() only stops a panic when called directly inside a deferred function; calling it anywhere else silently does nothing.
  • Modern Go optimizes common defer patterns heavily, so use it freely for correctness — reserve manual inline cleanup for genuinely hot loops.