11. `defer`: cleanup that always runs

📖 Reading · 8 min
💡 Most code boxes below are live — edit one and hit Run. Boxes without a Run button are reference-only (they can't run in your browser).

defer is a small Go keyword with an outsized impact on how clean Go code reads. It schedules a function call to run when the surrounding function is about to return — no matter how it returns.

The idea

package main

import "fmt"

func main() {
    defer fmt.Println("...world")
    fmt.Println("hello...")
}

Run it: hello... prints first, then ...world. The deferred call was scheduled on the defer line but executed at the very end of main.

Why it matters

The point is putting cleanup right next to the setup it undoes. In real code you open something — a file, a network connection, a lock — and you must close it later. defer lets you write the close immediately, so you can't forget it and it survives every exit path:

func readConfig() error {
    f, err := os.Open("config.yaml")
    if err != nil {
        return err
    }
    defer f.Close() // guaranteed to run, however this function returns

    // ... lots of code with many possible `return`s ...
    // you never have to remember to close f again
    return nil
}

(That box is reference-only — it touches the filesystem, which the in-browser runner can't do. The behavior is what matters: f.Close() runs on every return below it.)

Deferred calls run in LIFO order

Defer several calls and they run in reverse order — last deferred, first to run. Think of stacking plates: the last one you put on is the first you take off.

package main

import "fmt"

func main() {
    fmt.Println("start")
    defer fmt.Println("1 (deferred first)")
    defer fmt.Println("2")
    defer fmt.Println("3 (deferred last)")
    fmt.Println("end")
}

The output is start, end, 3, 2, 1. Reverse order is exactly what you want for cleanup: you tear down in the opposite order you set up.

Arguments are evaluated when defer runs, not later

A subtle, important detail: the arguments to a deferred call are captured at the moment you write defer, even though the call itself happens later.

func main() {
    x := 1
    defer fmt.Println("deferred sees x =", x) // captures 1 right now
    x = 99
    fmt.Println("final x =", x)
}

// final x = 99
// deferred sees x = 1

The deferred line prints 1, not 99 — the value of x was frozen at the defer statement. (If you need it to see the final value, defer a closure instead — a technique we'll cover with functions.)

That box is reference-only, and for an unusually interesting reason: this is the one place where the interpreter running these lessons disagrees with real Go. It evaluates the arguments late and prints 99. Compiled Go freezes them at the defer, exactly as described above — so trust the comment, not an experiment, on this one.

The everyday pattern

You'll see this shape constantly in Go: acquire, defer the release, then do the work in between.

package main

import "fmt"

func process(name string) {
    fmt.Printf("→ opening %s\n", name)
    defer fmt.Printf("← closing %s\n", name)

    fmt.Printf("  working with %s\n", name)
}

func main() {
    process("database")
    process("cache")
}

Each process call cleanly opens, works, and closes — and the close is never more than one line away from the open.

Your turn

Complete the program so it prints exactly:

task started
task finished
cleaning up

Use a single defer for the cleaning up line, placed at the top of main, so it runs last automatically.

package main

import "fmt"

func main() {
    // defer the "cleaning up" line here
    fmt.Println("task started")
    fmt.Println("task finished")
}
package main

import "fmt"

func main() {
    defer fmt.Println("cleaning up")
    fmt.Println("task started")
    fmt.Println("task finished")
}

That wraps up control flow. You can now branch, loop, and clean up — the backbone of every program. Next module: functions, where Go's real character starts to show.