defer

A defer statement schedules a function call to run when the surrounding function returns — however it returns, even through an early return or a panic. It's Go's way of guaranteeing clean-up: closing files, unlocking mutexes, finishing timers. This lesson covers how deferred calls are ordered, when their arguments are evaluated, common uses, and the pitfalls.

Running code on the way out

GO
package main

import "fmt"

func main() {
	fmt.Println("start")
	defer fmt.Println("deferred: runs last")
	fmt.Println("working...")
}
Output
start
working...
deferred: runs last

The deferred call waits until main is about to return. The great advantage is placement: the clean-up sits right next to the thing that needs cleaning up, so you can't forget it when you add another return later.

Closing resources

The classic pattern: acquire a resource, check the error, then immediately defer its release:

GO
package main

import (
	"bufio"
	"fmt"
	"os"
)

func countLines(path string) (int, error) {
	f, err := os.Open(path)
	if err != nil {
		return 0, err
	}
	defer f.Close() // runs on every return below

	n := 0
	sc := bufio.NewScanner(f)
	for sc.Scan() {
		n++
	}
	return n, sc.Err()
}

func main() {
	os.WriteFile("notes.txt", []byte("one\ntwo\nthree\n"), 0o644)
	fmt.Println(countLines("notes.txt"))
	fmt.Println(countLines("missing.txt"))
}
Output
3 <nil>
0 open missing.txt: no such file or directory

Defer only after checking the error: if os.Open failed, there's no file to close.

Last in, first out

A function can defer several calls. They run in reverse order — like a stack — which is exactly right when later resources depend on earlier ones:

GO
package main

import "fmt"

func main() {
	for i := range 3 {
		defer fmt.Println("deferred", i)
	}
	fmt.Println("loop done")
}
Output
loop done
deferred 2
deferred 1
deferred 0

Arguments are evaluated immediately

The function call is delayed, but its arguments are worked out when the defer statement runs. To use a variable's value at return time, defer a closure instead:

GO
package main

import "fmt"

func main() {
	status := "starting"
	defer fmt.Println("argument captured at defer time:", status)
	defer func() { fmt.Println("closure sees the final value:", status) }()
	status = "finished"
}
Output
closure sees the final value: finished
argument captured at defer time: starting

Measuring how long a function takes

Combining both rules gives a neat timing helper: trace runs immediately (it's an argument) and returns a function that runs at the end:

GO
package main

import (
	"fmt"
	"time"
)

func trace(name string) func() {
	start := time.Now()
	fmt.Println("enter", name)
	return func() {
		elapsed := time.Since(start)
		fmt.Println("exit", name, elapsed >= 20*time.Millisecond)
	}
}

func slowWork() {
	defer trace("slowWork")()
	time.Sleep(20 * time.Millisecond)
}

func main() {
	slowWork()
}
Output
enter slowWork
exit slowWork true

Note the two pairs of parentheses in defer trace("slowWork")(): the first call happens now, the returned function is deferred. (Real code would print elapsed; here it prints a comparison so the output is the same every run.)

Changing results with defer

A deferred closure runs after the return statement has set the results, but before the caller receives them. With named results it can inspect and change them — commonly to add context to an error:

GO
package main

import (
	"errors"
	"fmt"
)

func loadConfig(name string) (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("loading config %q: %w", name, err)
		}
	}()
	if name == "" {
		return errors.New("empty name")
	}
	return nil
}

func main() {
	fmt.Println(loadConfig("app.toml"))
	fmt.Println(loadConfig(""))
}
Output
<nil>
loading config "": empty name

Pitfall: defer in a loop

Deferred calls run when the function returns, not at the end of each loop iteration. Deferring Close inside a loop that opens thousands of files keeps them all open until the end. Move the body into its own function so each defer runs per item:

GO
package main

import (
	"fmt"
	"os"
)

func size(path string) (int64, error) {
	f, err := os.Open(path)
	if err != nil {
		return 0, err
	}
	defer f.Close() // closed when size returns, once per file
	info, err := f.Stat()
	if err != nil {
		return 0, err
	}
	return info.Size(), nil
}

func main() {
	os.WriteFile("a.txt", []byte("hello"), 0o644)
	os.WriteFile("b.txt", []byte("hello, world"), 0o644)
	for _, p := range []string{"a.txt", "b.txt"} {
		n, err := size(p)
		fmt.Println(p, n, err)
	}
}
Output
a.txt 5 <nil>
b.txt 12 <nil>

Pitfall: ignored errors from Close

defer f.Close() throws away Close's error. That's fine for files you only read. When writing, Close can report that data failed to reach the disk, so check it — for example with the named-result technique above, or by calling Close explicitly at the end and returning its error.

Try it yourself

Predict the output, then run it. Which line prints first, and why?

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
evaluating A
body
evaluating B
deferred closure: B
deferred: A
Next: functions that call themselves.