1. Why you need defer
If you are just starting out, the idea to “defer a function call” sounds like something very suspicious. Like, “why not just write cleanup() at the end and live peacefully?” The problem is that “at the end” is not a single point. In real code you often have several returns: you validate input somewhere, hit an error somewhere else, a condition does not match somewhere—so you exit early. defer solves exactly this: it lets you put a “mandatory action on exit” next to the place where it becomes mandatory.
A simple mental model is this: defer means “remind me to do this right before leaving the function”. And “before leaving” means before leaving the current function, not main, not the program, not “sometime later”, but strictly at the moment the function finishes its work.
In the official description of defer behavior, the wording is very simple: defer pushes deferred calls, and then executes them after the function has decided to return. Two rules are stated there and are key today: arguments are evaluated immediately, and calls are executed in LIFO order.
2. When defer runs
The most common thinking mistake with defer is this: a person sees defer and subconsciously expects it to run “after the current line” (as if it were some tricky ;). But defer is not about lines. defer is about leaving the function. That means the deferred call runs both on a “normal” completion and on an early return from any branch.
Let’s look at a small example with an early exit. Here defer prints the string "finish" regardless of whether we exit due to an error:
package main
import (
"errors"
"fmt"
)
func checkAge(age int) error {
fmt.Println("start") // start
defer fmt.Println("finish") // finish (runs on exit)
if age < 0 {
return errors.New("age must be >= 0")
}
return nil
}
func main() {
fmt.Println(checkAge(-10)) // age must be >= 0
}
It is important to notice that "finish" is printed after the function has decided to return an error, but before control fully transfers back to the caller.
If you want a visual model, it is roughly like this:
flowchart TD
A[execute function body] --> B{hit return?}
B -->|no| A
B -->|yes| C[run deferred calls LIFO]
C --> D[function actually returns values]
Until we reach return, defer is “kept in the function’s pocket” and waits. As soon as the function is about to leave—defer triggers.
You put a call into defer
Now we move to a detail that looks small but regularly hurts in practice. defer works with a function call. The correct form is defer something(...). This matters because defer must know what exactly to call and with which arguments.
Here is a correct example:
package main
import "fmt"
func say(msg string) {
fmt.Println(msg)
}
func main() {
defer say("bye") // bye
fmt.Println("hello") // hello
}
But you cannot do this:
// defer say // not allowed: defer expects a call, not a name
This restriction is part of the model: defer pushes a “ready-to-run call” onto a stack. And now we are approaching the main topic of the lecture: when exactly the arguments of that call are captured and in what order multiple defers execute.
3. Arguments and execution order of defer
defer arguments are evaluated immediately
Here is a rule you should not only memorize but feel in your bones: arguments of a deferred call are evaluated at the moment the defer statement executes, not at the moment the function exits.
This means that defer fmt.Println(i) “remembers” the current value of i right now, even if i changes ten times later.
Example:
package main
import "fmt"
func main() {
i := 0
defer fmt.Println("defer i =", i) // defer i = 0
i = 10
fmt.Println("now i =", i) // now i = 10
}
If you expected "defer i = 10", then you had the wrong model. The key habit: defer captures arguments immediately, like a snapshot.
Mini table: “what is evaluated now vs later”
| What happens | When it happens |
|---|---|
| Arguments of f(x, y) in defer f(x, y) are evaluated | immediately, when the line with defer executes |
| The call itself f(...) is executed | later, when leaving the function |
| Order of multiple defers | LIFO: the last added runs first |
Multiple defers: LIFO order
Now the second rule, without which defer becomes fortune-telling: if there are multiple defers in a function, they execute in reverse order. Like a stack of plates: the last one placed on top is the first one taken off. That is called LIFO: Last In, First Out.
Here is an example—straight to the point:
package main
import "fmt"
func main() {
defer fmt.Print("A") // runs third
defer fmt.Print("B") // runs second
defer fmt.Print("C") // runs first
}
The result will be:
CBA
Why? Because the “stack of deferred calls” is LIFO.
defer in a loop: a separate pitfall
It is easy to draw the wrong conclusion here: “okay, so defer in a loop runs at the end of the iteration.” No. It runs at the end of the function. And if you put a hundred defers in a loop, then at the end of the function you will get a hundred calls (in reverse order). Sometimes that is exactly what you want, and sometimes it unexpectedly turns the program into a “printing press”.
Let’s show this on a tiny piece of code:
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
defer fmt.Print(i) // 210
}
}
You will see 210 on output, because i=0 was deferred first (it is at the bottom of the stack), and i=2 was deferred last (it is on top), so it executes earlier.
defer and an anonymous function: where the closure “hides”
A common situation: you hear “arguments are evaluated immediately”, and then you write code and see “for some reason it prints the last value”. This is not a contradiction; it is important to distinguish two things: call arguments and a closure that reads a variable later.
If you do defer fmt.Println(i), then i is a call argument and it is evaluated immediately.
If you do defer func() { fmt.Println(i) }(), then this anonymous function has no arguments, and it takes i from the outer scope, meaning it reads the variable when the deferred function executes, not when defer is set up.
Let’s compare two almost identical fragments:
package main
import "fmt"
func main() {
i := 0
defer fmt.Println("arg:", i) // arg: 0
defer func() {
fmt.Println("closure:", i) // closure: 10
}()
i = 10
}
Here both defers are “next to each other”, but behave differently: fmt.Println("arg:", i) captures i immediately and remembers 0.
The anonymous function closes over i and will read it later, when i is already 10.
How to make an anonymous function “remember the value now”
You need to pass the value as a parameter (turning “reading from a closure” into a “call argument” that is evaluated immediately):
package main
import "fmt"
func main() {
i := 0
defer func(v int) {
fmt.Println("v =", v) // v = 0
}(i)
i = 10
}
This is a good pattern if you want to use defer with an anonymous function while keeping the predictable “the value is captured at the moment of defer”.
4. Example: a mini calculator with defer
Now let’s tie defer to the programming style you start building from day one: read input, parse, compute, print. We will not use a “real resource” like a file (that is a separate topic later), but we will do something very practical: a “entered the function / left the function” log. This helps with debugging even in simple tasks, and defer makes it almost free.
Imagine we have a function compute that performs an operation and may return an error (division by zero, unknown operation). We want it to always print "compute: end" at the end, even if we exit due to an error.
package main
import (
"errors"
"fmt"
)
func compute(op string, a, b int) (int, error) {
fmt.Println("compute: start") // compute: start
defer fmt.Println("compute: end") // compute: end
if op == "/" && b == 0 {
return 0, errors.New("division by zero")
}
if op == "+" {
return a + b, nil
}
if op == "/" {
return a / b, nil
}
return 0, errors.New("unknown operation")
}
func main() {
res, err := compute("/", 10, 0)
fmt.Println("res =", res, "err =", err) // res = 0 err = division by zero
}
The main idea of the example: defer gives us a guaranteed function “tail”. We do not write fmt.Println("compute: end") manually in every return. We write it once and forget about it.
5. Mental model of the defer stack
With defer, the habit of “playing through” the code in your head helps a lot. Especially in interviews, but also in day-to-day work—fewer surprises. For this it is useful to keep one simple picture: each function has its own internal stack of deferred calls. When we encounter defer, we push a new call onto the top. When the function leaves, we start popping from the top and executing.
Let’s intentionally write an example where it is easy to get it wrong if you do not keep the LIFO model in mind:
package main
import "fmt"
func demo() {
defer fmt.Print("1") // runs third
fmt.Print("A") // runs immediately
defer fmt.Print("2") // runs second
fmt.Print("B") // runs immediately
defer fmt.Print("3") // runs first
fmt.Print("C") // runs immediately
}
func main() {
demo() // ABC321
}
It is useful to read it like this: “Now we print A, B, C. And then, on exit from demo(), we print 3, 2, 1.” Result: "ABC321".
This predictability is why defer is considered an honest tool: it does not require telepathy if you know the two rules (arguments now, calls LIFO).
6. Typical mistakes when working with defer
Mistake #1: expecting defer to run “right after the line”.
This kind of bug often appears after languages that have finally, or where people are used to “cleanup at the end of the block”. In Go, defer is tied to the function: until the function exits, defer does not execute. If you put defer in a loop, it will not run “at the end of the iteration”—it will wait for the function to exit entirely.
Mistake #2: thinking defer will “take the latest argument values at the end”.
The rule is the opposite: arguments are evaluated immediately, at the moment defer executes. This is especially visible with variables that change later. If you need deferred code to see the “latest state”, do not pass that value as an argument; read it inside a closure—but do it consciously, understanding the difference.
Mistake #3: mixing up “defer arguments” and “a closure”, then being surprised by the output.
When you write defer fmt.Println(i), i is captured immediately. When you write defer func(){ fmt.Println(i) }(), there are no arguments, and the variable is read later. As a result, one defer prints the “old” value and the other prints the “new” one—this is not a Go bug; they are different mechanisms.
Mistake #4: putting many defers in large loops and not thinking about the consequences.
Yes, defer is convenient. But if you place ten thousand defers inside a loop, you will have ten thousand deferred calls that only run when leaving the function. Even without talking about performance, this can make logs unreadable and complicate understanding the order of actions. In such places it is usually better to either move the defer into a wrapper function for a single iteration, or explicitly perform the action at the end of the iteration (if that is what you actually need).
Mistake #5: relying on “natural order” instead of LIFO and getting the reverse order.
If your mental model is “it will run top-to-bottom”, all examples with defer will look “broken”. In Go, the order is strictly LIFO: the last defer runs first. This is especially important when one action logically depends on another (for example, if you “acquired A, then acquired B”, you often want to “release” in the order “B first, then A”—and LIFO makes this natural).
GO TO FULL VERSION