Menu

Golang defer: Order, Argument Evaluation and Pitfalls

defer schedules a call to run when the surrounding function returns. Learn LIFO order, when arguments are evaluated, closing files and unlocking mutexes, defer in loops, and changing named results.

This page includes runnable editors - edit, run, and see output instantly.

What defer does

defer pushes a function call onto a list. When the surrounding function returns, the list runs in reverse order.

Output:

start
end
deferred 3
deferred 2
deferred 1

The order is last in, first out, like a stack. That matches how resources nest: if you open A and then B, you usually want to close B before A.

Cleanup next to acquisition

The main use of defer is to write the cleanup on the line right after the acquisition, so no return path can forget it.

Notice the order: check the error first, then defer. If os.Open failed, f is nil and deferring f.Close() before the check would call Close on a nil *os.File (which returns an error you never see, and is misleading to readers).

The same shape works for locks:

mu.Lock()
defer mu.Unlock()

If the code between them panics, the mutex is still unlocked.

Arguments are evaluated immediately

The deferred function and its arguments are evaluated when the defer statement runs. Only the call itself waits.

Output:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) captured the value 1 at the defer line. The closure has no arguments; it reads x when it finally runs. Pick the form that matches what you want to record.

This also applies to method receivers. defer t.Stop() evaluates t right away, so reassigning t later does not change which value gets stopped.

A common timing trick uses this rule on purpose:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") is called immediately (it can print "enter" and record the start time), and the function it returns is what gets deferred.

Defer in a loop

Deferred calls run when the function returns, not at the end of each loop iteration. In a loop over many files, this keeps every file open until the function ends.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

With thousands of paths this runs out of file descriptors. Move the body into its own function so each defer runs per iteration:

Each call to processFile closes its file before the next one opens. A function literal called in place (func() { ... }()) works the same way when a named helper feels like too much.

Changing return values

A deferred closure runs after the return statement has assigned the results, and it can modify named results before the caller sees them.

This prints 10 and save failed: disk full. With an unnamed result, a deferred function can still run, but it has no way to change what is returned.

Capturing the error from Close

defer f.Close() throws away the error from Close. For files you only read, that is fine. For files you wrote, Close can report a failed final flush, so the error matters. A named result lets you keep it:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) is a shorter alternative when you want both errors reported.

Defer, panic and recover

Deferred calls run while a panic unwinds the stack. That is the only place recover does anything, and it is how a server keeps one bad request from killing the process. The details are on panic and recover.

Deferred calls do not run when the program exits through os.Exit or log.Fatal. If main defers cleanup and then calls os.Exit(1), the cleanup is skipped.

Cost

Since Go 1.14, most defers are open-coded by the compiler and cost a few nanoseconds. Using defer for every mutex release and file close is the normal style. Defers inside loops are the exception: those cannot be open-coded and fall back to a slower path, which is one more reason to move loop bodies into functions.

Common mistakes

  • Deferring before the error check. Check err from Open first, then defer Close.
  • Expecting per-iteration cleanup in a loop. Defers run at function exit.
  • Expecting a deferred argument to see later changes. Arguments are fixed at the defer line. Use a closure to read at exit.
  • Relying on defer with os.Exit. It never runs.

Frequently Asked Questions

What does defer do in Go?

defer f() schedules f() to run when the surrounding function returns, whether it returns normally, through an early return, or by panicking. It is used to put cleanup (closing a file, unlocking a mutex) right next to the code that acquired the resource.

In what order do deferred calls run in Go?

Last in, first out. The most recently deferred call runs first. defer fmt.Println(1); defer fmt.Println(2) prints 2 and then 1.

When are the arguments of a deferred function evaluated?

Immediately, when the defer statement executes, not when the call runs. x := 1; defer fmt.Println(x); x = 2 prints 1. To read the value at exit time, defer a closure: defer func() { fmt.Println(x) }().

Does defer run on panic or os.Exit?

Deferred calls run while a panic unwinds the stack, which is why recover works inside them. They do not run when the program calls os.Exit (or log.Fatal, which calls it), and they do not run in other goroutines when main returns.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED