Cleanup Ordering — Junior Level¶
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Pros & Cons
- Use Cases
- Code Examples
- Coding Patterns
- Clean Code
- Product Use / Feature
- Error Handling
- Security Considerations
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Common Misconceptions
- Tricky Points
- Test
- Tricky Questions
- Cheat Sheet
- Self-Assessment Checklist
- Summary
- What You Can Build
- Further Reading
- Related Topics
- Diagrams & Visual Aids
Introduction¶
Focus: "Why does Go have
defer? In what order do my cleanups run? When does a deferred function actually execute?"
When you open a file, acquire a lock, or start a network connection in Go, you also take on a debt: you must release that resource later. The defer statement is Go's primary tool for paying that debt. It says: "no matter how this function exits — return, panic, or early exit — run this cleanup before leaving."
That line is one of the first idioms every Go programmer learns. It looks innocent: schedule a close, get on with the work. But behind the simplicity is a precise set of rules. Deferred calls run in LIFO order (last-in, first-out). Their arguments are evaluated at the moment of the defer statement, not at the moment they actually run. They run even when the function panics. And — more subtly — they can themselves modify the function's named return value, swallow errors, or call other deferred functions that run in their own order.
This sub-topic begins at that idiom and works outward. By the end of this junior file you will be able to:
- Read a function and predict the exact order in which its deferred calls fire
- Explain why
deferarguments are evaluated at thedeferline, not at function exit - Use
deferto close files, unlock mutexes, and stop tickers safely - Avoid the most common rookie traps: defer inside a loop, ignored errors from
Close, deferred calls that capture stale variables - Understand the rough relationship between
deferandcontextcancellation — enough to spot the difference between "cancel signals" and "actual cleanup" - Recognise that a goroutine's deferred functions only run when that goroutine exits, not when its parent exits
You do not yet need to understand context.AfterFunc, open-coded defer optimisation, the runtime.deferproc machinery, panic/recover during cleanup, or the design of resource hierarchies spanning multiple packages. Those belong to the middle, senior, and professional files. This file is about the moment your function says "I am leaving" and a small stack of cleanup calls unwinds in the reverse order they were registered.
We will keep the examples short, runnable, and faithful to real Go behaviour. Every snippet in this file compiles. If a snippet shows a deliberate bug, it will be clearly labelled // BUG:.
Prerequisites¶
- Required: Go 1.21 or newer installed. Check with
go version. Some examples usecontext.AfterFunc, which appeared in 1.21. - Required: Comfort writing and running a
mainfunction and a few helpers in one file. - Required: Familiarity with
os.Open,os.Create,*os.File, and theio.Reader/io.Closerinterfaces. You should know thatClose()can return an error. - Required: Familiarity with
sync.Mutexandsync.WaitGroupat the level of "I can lock, unlock, and wait." - Helpful: A passing acquaintance with
context.Contextand the idea thatcancel()is itself a function you call. - Helpful: Awareness that panics in Go are recoverable from a deferred function — we will not dwell on
recoverhere, but it shows up.
If defer fmt.Println("done") makes sense to you and you have ever opened a file with os.Open, you are ready.
Glossary¶
| Term | Definition |
|---|---|
defer | A statement that registers a function call to run when the surrounding function returns, panics, or otherwise unwinds. Syntax: defer Call(args). |
| Defer stack | The per-goroutine, per-function stack of pending deferred calls. New defers are pushed on top; on function exit they pop from the top down — LIFO order. |
| LIFO | "Last in, first out" — the same pattern as a stack of plates. Crucial for cleanup because resources acquired in order A → B → C should be released in order C → B → A. |
| Cleanup | Code that releases a resource, ends a transaction, flushes a buffer, or otherwise restores invariants the function disturbed. |
Close() | The conventional method name for releasing a resource. Defined by the io.Closer interface. Returns an error because closing can fail. |
Stop() | The conventional method name for stopping a timer, ticker, or background routine. |
| Named return value | A return value declared in the function signature, e.g. func read() (err error). Deferred functions can read and modify it. |
| Resource hierarchy | An ordered set of resources where releasing one in the wrong order produces a use-after-close, a leaked goroutine, or a corrupted state. |
| Idempotent cleanup | Cleanup that is safe to call more than once. os.File.Close returns an error on the second call but does not crash; some Close methods crash. |
| Open-coded defer | A compiler optimisation that inlines a small number of defers into the function body rather than allocating a runtime defer record. Reduces overhead. |
context.AfterFunc | A Go 1.21 hook: stop := context.AfterFunc(ctx, fn). Runs fn in its own goroutine after ctx is cancelled. Useful for cleanup that must survive the parent goroutine. |
| Goroutine exit | The moment a goroutine's top-level function returns. Only at that moment do that goroutine's deferred calls fire — not the parent's. |
Core Concepts¶
defer registers a call; it does not call it yet¶
The single line
does two things and only two things:
- It evaluates the receiver
file(and any argument expressions) now, at the point of thedeferstatement. - It pushes "call
Close()on this value" onto the goroutine's defer stack for this function.
It does not call Close yet. The call happens when the function is about to return — by any path. That includes a return statement, the end of the function body, and a panic.
func read() error {
file, err := os.Open("data.txt")
if err != nil {
return err
}
defer file.Close() // <-- registered here, but not run yet
data, err := io.ReadAll(file)
if err != nil {
return err // Close runs as part of returning
}
process(data)
return nil // Close also runs here
}
LIFO order¶
If a function registers three defers in order A, B, C, they run in order C, B, A:
func three() {
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
fmt.Println("body")
}
Output:
Why LIFO? Because resources have nesting structure. You open a file, then inside that file you start a buffered reader. The reader must be flushed before the file is closed, or the flush writes into a closed file. By deferring in acquisition order, LIFO unwinding gives you the right release order for free.
Argument capture: evaluated at defer, called later¶
func surprise() {
x := 1
defer fmt.Println("deferred sees x =", x)
x = 99
fmt.Println("body sees x =", x)
}
Output:
The argument x was evaluated at the moment the defer statement executed. Even though Println itself runs much later, it sees the value 1, not 99.
This is the single most common source of confusion for newcomers. If you want to defer a function that reads the latest value of x, wrap it in a closure:
Now the closure captures x by reference (well, by closure over the variable), and prints 99.
Defer runs on every exit path — including panics¶
Output before the panic crashes the program:
This is what makes defer reliable: you do not have to remember to put Close() in every return branch and in every error path and in every conditional. A single defer covers them all.
The only exit that skips defers is os.Exit (and the runtime's exit on unrecovered fatal errors). Plain panics, unrecovered or not, do run defers.
Defers belong to the goroutine that registered them¶
A deferred call attached to f only runs when f itself returns. If f spawns a goroutine that calls g, the defers inside g belong to that new goroutine and run when g returns, not when f returns.
func parent() {
defer fmt.Println("parent done")
go child()
time.Sleep(50 * time.Millisecond)
fmt.Println("parent returning")
}
func child() {
defer fmt.Println("child done")
fmt.Println("child running")
}
parent done fires when parent returns. child done fires whenever child returns — which could be before, after, or never (if child blocks forever) relative to parent. This is the source of the "spawned goroutine leak" pattern: registering a defer in a goroutine that never exits is the same as never registering it at all.
Context cancellation is not cleanup¶
A subtlety that confuses many Go newcomers: calling cancel() on a context.CancelFunc does not clean up your resources. It only sends a signal — it closes ctx.Done(). Your code must observe that signal and release whatever it owns.
ctx, cancel := context.WithTimeout(parent, time.Second)
defer cancel() // releases the timer; does not close your file
The defer cancel() is itself important — it releases the goroutine the timer holds — but if your function also opened a file or acquired a lock, those need their own defers.
We will explore the relationship between context cancellation and cleanup more in the middle and senior files. For now, the message is: defer cancel() is one defer among many; do not let it stand in for resource cleanup.
Real-World Analogies¶
Plates in a sink¶
Imagine washing dishes. You stack them in the sink in the order you used them: dinner plate first, salad plate on top, dessert plate last. When you wash them, you take them off the top of the stack — the dessert plate first, then the salad plate, then the dinner plate. That is LIFO and it is exactly how defer runs.
If you tried to wash the bottom plate first, you would have to move the others out of the way. Software has the same problem: you cannot close a database connection that a transaction is still using; the transaction (acquired after the connection) must close first.
Boarding and leaving an aeroplane¶
When you board, you put your bag in the overhead bin (resource 1), buckle your seatbelt (resource 2), put on your headphones (resource 3). When the flight ends, you unwind in reverse: take off headphones, unbuckle, retrieve bag. Try the reverse order and you find yourself standing in the aisle holding three things at once with your seatbelt fastened.
A nested set of contracts¶
A construction contract may have sub-contracts. The general contractor signs first, then sub-contracts the plumbing, then sub-contracts the electrics. When the project ends, the electrician's contract closes first, then the plumber's, then the general contractor's. The order matters because the sub-contracts depend on the parent contract being in force.
Undoing a recipe¶
If a recipe says "preheat the oven, melt the butter, mix the dough, bake," cleaning up means turning off the oven (last), washing the bowl (mid), and putting the butter back (first). LIFO again — and missing the last step (turn off the oven) is the kind of bug that runs the gas bill up overnight.
Mental Models¶
Model 1: The defer stack as a pending-work list¶
Picture each goroutine's current function as having an invisible to-do list. Every defer statement appends an item to the top of that list. When the function returns, the runtime pops items off the top one at a time and runs them. The function only truly exits after the list is empty.
function body executes
defer A [A]
defer B [B, A]
defer C [C, B, A]
return triggers unwinding:
pop C, run C [B, A]
pop B, run B [A]
pop A, run A []
function actually returns to caller
Model 2: Frozen arguments, deferred call¶
Each defer record stores: - A function pointer - A snapshot of the argument values at registration time - A link to the next defer record
The function pointer is not called at registration time; the arguments are evaluated. This is why defer fmt.Println(x) prints the value of x at the defer site, even if x changes later.
Model 3: defer as a guard¶
Think of defer X() as installing a guard at the function's exit door. No matter how you try to leave — return, panic, os.PathError — the guard executes before the door opens. There can be multiple guards (LIFO), and each one only sees the world as it was when it was installed (for arguments) or as it is now (for closure captures).
Model 4: Lifetimes and ownership¶
A function f owns every resource it acquires until it explicitly transfers ownership. defer Close() is a vow: "I will release this before I leave." A function that returns a still-open resource is transferring ownership to the caller — and must not defer-close it.
// owns the file; closes before returning
func processFile(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
return process(f)
}
// transfers ownership; caller must close
func openFile(path string) (*os.File, error) {
return os.Open(path)
}
Mixing these two patterns — deferring a close on a resource you are about to return — is a use-after-close bug.
Pros & Cons¶
Pros of defer¶
- Centralised cleanup. One line at the top of a function ensures the cleanup runs on every exit path.
- Panic-safe. Cleanup runs even when the function panics, preventing leaks during failure.
- Reads top-to-bottom. Acquisition and intended cleanup sit next to each other in code, making intent obvious.
- LIFO ordering for free. The natural acquisition order produces the natural release order without any extra bookkeeping.
- Compiler-optimised in many cases. Modern Go inlines short defers ("open-coded defer") so the runtime cost is negligible.
Cons / costs of defer¶
- Not free. A defer that the compiler cannot open-code allocates a small record on the heap and follows a linked list at exit. Hot loops with one defer per iteration can show up in profiles.
- Easy to misuse in loops.
for ... { defer file.Close() }registers a defer per iteration; all of them run only when the enclosing function exits, which is a leak. - Silent error swallowing.
defer f.Close()discards the error fromClose. Writers that buffer (likebufio.Writerover a file) can lose data this way. - Hidden control flow. A reader who skims a function may miss that the defer mutates a named return value — making it look as if the function returns one thing when in fact it returns another.
We expand on each of these in the relevant sections below.
Use Cases¶
defer is the right tool for almost any "acquire / release" pair. Common cases:
- File handles.
f, _ := os.Open(...); defer f.Close() - Mutex locks.
mu.Lock(); defer mu.Unlock() - Database transactions.
tx, _ := db.BeginTx(...); defer tx.Rollback()(with a subsequentCommitthat nullifies the rollback) - HTTP response bodies.
resp, _ := http.Get(...); defer resp.Body.Close() - Timers and tickers.
t := time.NewTicker(...); defer t.Stop() - Context cancellation.
ctx, cancel := context.WithTimeout(...); defer cancel() sync.WaitGroup.Doneat the top of a goroutine:defer wg.Done()ensures the counter decrements even on panic.- Tracing / metrics.
defer trace.End(),defer func() { metrics.Observe(time.Since(start)) }(). - Recovering from panics in goroutines.
defer func() { if r := recover(); r != nil { log.Print(r) } }(). - Restoring state in tests.
oldEnv := os.Getenv("X"); os.Setenv("X", "test"); defer os.Setenv("X", oldEnv).
When defer is not the right tool: - When the resource must be released before the function exits (e.g. you need to use it, release it, then continue computation). - When cleanup must survive the goroutine that registered it (use context.AfterFunc or a shutdown channel). - When you need fine-grained ordering control — for example, releasing in batches, or releasing the first resource before the others. Then you call cleanup functions manually.
Code Examples¶
All examples in this section are complete, runnable Go programs. Save each as main.go, run with go run main.go.
Example 1: A single defer¶
package main
import "fmt"
func main() {
fmt.Println("start")
defer fmt.Println("cleanup")
fmt.Println("middle")
}
Output:
The deferred Println("cleanup") runs after the function body finishes — last, even though it was written second.
Example 2: LIFO with three defers¶
package main
import "fmt"
func main() {
defer fmt.Println("first defer")
defer fmt.Println("second defer")
defer fmt.Println("third defer")
fmt.Println("body")
}
Output:
Example 3: Argument evaluation timing¶
package main
import "fmt"
func main() {
x := 10
defer fmt.Println("deferred:", x) // captures 10 NOW
x = 99
fmt.Println("body:", x)
}
Output:
The deferred call printed 10, the value of x at the moment of the defer statement.
Example 4: Closure captures by reference¶
package main
import "fmt"
func main() {
x := 10
defer func() { fmt.Println("deferred:", x) }()
x = 99
fmt.Println("body:", x)
}
Output:
A closure captures the variable, not its value. By the time the closure runs, x is 99.
Example 5: Closing a file¶
package main
import (
"fmt"
"io"
"os"
)
func main() {
f, err := os.Open("/etc/hostname")
if err != nil {
fmt.Println("open:", err)
return
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
fmt.Println("read:", err)
return
}
fmt.Printf("read %d bytes\n", len(data))
}
The defer guarantees f.Close() runs whether the read succeeds, fails, or panics.
Example 6: Mutex unlock¶
package main
import (
"fmt"
"sync"
)
var (
mu sync.Mutex
counter int
)
func bump() {
mu.Lock()
defer mu.Unlock()
counter++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
bump()
}()
}
wg.Wait()
fmt.Println("counter =", counter)
}
defer mu.Unlock() is the right way to release a mutex in any non-trivial function. If the function panics mid-update, the mutex still unlocks, and other goroutines do not deadlock.
Example 7: Panic-safe cleanup¶
package main
import "fmt"
func mayPanic() {
defer fmt.Println("cleanup ran")
panic("oh no")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
mayPanic()
}
Output:
mayPanic's defer ran before the panic propagated up to main's recover.
Example 8: Returning an error captured by defer¶
package main
import (
"errors"
"fmt"
)
func work() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("work failed: %w", err)
}
}()
return errors.New("disk full")
}
func main() {
fmt.Println(work())
}
Output:
The deferred closure wraps the named return value err. Without err as a named return, the closure could not see or modify the error.
Example 9: Stopping a ticker¶
package main
import (
"fmt"
"time"
)
func main() {
t := time.NewTicker(50 * time.Millisecond)
defer t.Stop()
for i := 0; i < 5; i++ {
<-t.C
fmt.Println("tick", i)
}
}
defer t.Stop() releases the underlying timer resource. Without it, the ticker leaks until garbage collection — and even then, only because newer Go versions hooked tickers into the GC.
Example 10: A goroutine's defer fires when that goroutine exits¶
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer fmt.Println("goroutine cleanup")
fmt.Println("goroutine work")
}()
fmt.Println("main waiting")
wg.Wait()
fmt.Println("main done")
}
Output:
The goroutine's defers ran when the goroutine returned, not when main returned. main's Wait() blocked until the goroutine was fully done — including its defers.
Example 11: defer inside a for loop — usually a bug¶
package main
import (
"fmt"
"os"
)
// BUG: defers stack up; all closes run only when ProcessAll returns.
func ProcessAll(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // <-- never runs until ProcessAll exits
_ = f
}
return nil
}
func main() {
fmt.Println("see junior.md commentary for the fix")
_ = ProcessAll
}
If paths has a million entries, ProcessAll holds a million open files until it returns. The fix is to lift the body into a helper that closes per iteration:
func ProcessAll(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_ = f
return nil
}
Example 12: Acquire two resources, release in reverse¶
package main
import (
"fmt"
"os"
)
func main() {
in, err := os.Open("/etc/hostname")
if err != nil {
fmt.Println(err); return
}
defer in.Close()
out, err := os.Create("/tmp/copy.txt")
if err != nil {
fmt.Println(err); return
}
defer out.Close()
// ... use both ...
fmt.Println("both open")
}
When main returns, out.Close() runs first (last defer registered → first popped), then in.Close(). LIFO matches the natural "release the inner resource first" rule.
Example 13: Defer with context.WithCancel¶
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // crucial: releases context's internal goroutine
go func() {
select {
case <-ctx.Done():
fmt.Println("worker: cancelled")
case <-time.After(time.Second):
fmt.Println("worker: timed out")
}
}()
time.Sleep(100 * time.Millisecond)
cancel() // signal early
time.Sleep(100 * time.Millisecond) // give worker a chance to print
}
Note that cancel is called explicitly (the first call wins; later calls are no-ops). The defer cancel() exists for the case where the function returns without reaching the explicit call.
Example 14: context.AfterFunc — Go 1.21¶
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stop := context.AfterFunc(ctx, func() {
fmt.Println("cleanup after cancel")
})
defer stop() // if we exit before cancel, deregister the AfterFunc
cancel() // schedule the AfterFunc
time.Sleep(50 * time.Millisecond)
}
AfterFunc runs its callback in a fresh goroutine the moment ctx is cancelled. The returned stop lets you deregister it if you no longer want it to fire. We cover this in depth in middle.md and senior.md.
Example 15: Tracking elapsed time¶
package main
import (
"fmt"
"time"
)
func main() {
defer func(start time.Time) {
fmt.Println("elapsed:", time.Since(start))
}(time.Now()) // argument evaluated now → start is captured
time.Sleep(120 * time.Millisecond)
}
The trick: passing time.Now() as an argument to the deferred function captures the starting instant at defer time. The closure then reads the captured start and computes the difference at exit.
Coding Patterns¶
Pattern 1: Acquire-and-defer immediately¶
Always put the defer on the line directly after a successful acquisition. Errors before the defer mean the resource was never acquired; errors after it are covered by the defer.
Pattern 2: Per-iteration cleanup via helper¶
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
func processOne(p string) error {
f, err := os.Open(p)
if err != nil { return err }
defer f.Close()
// ...
return nil
}
Each call to processOne has its own defer scope, so each file closes promptly.
Pattern 3: Defer-then-rollback for transactions¶
tx, err := db.BeginTx(ctx, nil)
if err != nil { return err }
defer tx.Rollback() // safe: Rollback after Commit is a no-op
// ... work ...
return tx.Commit()
A successful Commit makes the deferred Rollback a no-op. A return before Commit rolls back. Clean and resilient.
Pattern 4: Capture a named return value¶
func read(path string) (err error) {
f, oerr := os.Open(path)
if oerr != nil { return oerr }
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// ... read ...
return nil
}
If reading succeeded but Close failed (rare for read-only files, common for writes), the function still reports the close error.
Pattern 5: defer cancel() for every context.With*¶
The Go vet tool will warn if you forget. cancel is cheap and idempotent; defer it unconditionally.
Pattern 6: A one-line trace probe¶
defer trace("processOrder")()
func trace(name string) func() {
start := time.Now()
log.Printf("enter %s", name)
return func() {
log.Printf("exit %s after %s", name, time.Since(start))
}
}
The outer call returns a closure; the closure is then deferred. The result: a paired enter/exit log around any function.
Clean Code¶
A few practical guidelines for keeping defer-driven cleanup readable.
1. Defer immediately after acquisition¶
Don't separate the acquisition from the defer by ten lines of logic. The reader should not have to scan the function to be confident the cleanup is registered.
Bad:
Good:
2. Prefer one resource per function¶
Functions that acquire and clean up four different resources tend to grow complex defer stacks and become hard to reason about. Split them.
3. Name your return value when defers modify errors¶
The named return makes it obvious to readers that the deferred function can modify err.
4. Avoid defer in tight loops¶
If a function loops a million times and defers on each iteration, those defers stack up and only run at function exit. Either lift the body into a helper or call the cleanup explicitly.
5. Don't ignore errors from Close¶
For files you only read, Close is unlikely to fail in a way that matters. For files you write — and for things like bufio.Writer.Flush or transaction Commit — a failed Close can mean lost data. Handle it.
6. Document the order if it is not obvious¶
If three defers have an order that matters for correctness and not just for closure-of-resource, write a one-line comment explaining why.
Product Use / Feature¶
In a typical production Go service, cleanup ordering is the difference between a healthy shutdown and a cascade of "use of closed network connection" log lines. Concrete situations where order matters:
- HTTP server shutdown. Close the listener, then drain in-flight requests, then close database pools. Reverse the order and you cut connections to the database while requests still need it.
- Worker pool shutdown. Stop accepting new tasks, drain the queue, signal workers, wait for them to exit, then close logs and metrics. Reverse, and workers race the log writer.
- gRPC client lifecycle. Stop streaming RPCs first, then close the client connection, then close the credential source. Reverse, and the streams panic on a closed transport.
- Distributed tracing flush. The tracer's
Shutdownmust be deferred first — so it pops last — to ensure that all other defers' spans get flushed before the process exits.
You will see these patterns first-hand if you read the standard library: net/http's Server.Shutdown, database/sql's DB.Close, and the os/exec.Cmd lifecycle all reflect strict ordering rules.
Error Handling¶
Errors returned from Close¶
Most Close methods can return an error. defer f.Close() discards that error. For files opened only for reading, that is usually fine. For files opened for writing, or for any buffered writer, ignoring the error is a real bug because:
- The OS might still have un-flushed kernel buffers when
Closeruns. - A network filesystem might fail the flush.
- A
bufio.Writerover the file might still be holding bytes you never wrote.
The defensive pattern:
func write(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
}
Two things to notice: 1. The function uses a named return (err error) so the deferred closure can modify it. 2. The closure only overwrites err if it was nil — so a real write error is not masked by a close error.
Panics during cleanup¶
A deferred function can itself panic. When it does, the panic replaces any panic already in flight (if any), and unwinding continues with the new one. The Go specification calls this "the original panic value is discarded." In practice, this means a buggy Close that panics will hide the bug that actually caused the function to fail.
For now, just be aware. Recovery patterns are a middle-level topic.
Multiple errors¶
If you defer two cleanups and both can fail, you generally want to report both. Go 1.20 introduced errors.Join:
This avoids dropping either error. We expand on multi-error cleanup in middle.md.
Security Considerations¶
Cleanup order is not just hygiene; it can have security implications.
- Sensitive data in buffers. If you write secrets to a file and rely on
defer f.Close()to flush, but the program crashes earlier, the secret might never hit disk — or might end up only partially written and recoverable in unexpected places. - Stale TLS state. Closing a TLS connection involves sending a close-notify alert. If you skip it (because of out-of-order defers), the peer cannot distinguish your normal close from an attacker truncation. Always defer-close TLS connections.
- File permissions during write. Creating a file with
os.Createuses0666-with-umask. If you set explicit permissions afterwards (f.Chmod(0600)), put theChmodbefore writing sensitive data, not in a deferred call. Otherwise the data exists with wider permissions for an instant. - Lock release on panic. If a critical section panics with the lock still held but no defer to release it, every goroutine waiting on the lock is wedged. That is a denial-of-service. Always defer-unlock.
- Resource exhaustion as DoS. A defer-in-loop bug that leaks file descriptors will eventually exhaust the process's FD limit, taking the service down. Cleanup ordering is part of availability.
Performance Tips¶
- Open-coded defers are nearly free. The compiler inlines defers when a function has fewer than eight at compile time and none of them are inside a loop. The cost in those cases is a few extra instructions.
- Heap defers are slower. A
deferinside a loop or one of more than eight in a function falls back to a heap-allocated defer record. The cost is small but measurable in tight benchmarks. - Don't optimise prematurely. For 99% of code, the cost of
deferis invisible. Profile first; do not contort your code to avoid one defer per function. - Avoid
deferin hot loops if it shows up in a profile. Lift the body into a helper, or call cleanup explicitly. deferdoes not increase allocation by default. Open-coded defers do not allocate. Heap defers allocate exactly one record per defer.
Best Practices¶
- Defer the cleanup immediately after a successful acquisition.
- Use named return values when a defer modifies the returned error.
- Do not defer in a tight loop; extract a helper.
- Never assume
Closesucceeds — handle its error explicitly for writes. - For every
context.With*, defer the returnedcancel. - For long-lived resources owned by a goroutine, register the cleanup with
deferat the top of the goroutine, and make sure the goroutine actually exits. - Use
defer mu.Unlock()whenever the locked region is more than a couple of lines. - Use
defer trace("name")()as a quick way to add enter/exit logging without modifying function bodies.
Edge Cases & Pitfalls¶
os.Exit skips defers¶
Nothing prints. os.Exit terminates immediately. If you must os.Exit from a function that holds resources, release them manually first.
runtime.Goexit runs defers¶
runtime.Goexit ends the calling goroutine and runs its defers. Useful in testing (t.FailNow uses it).
Defers in init functions¶
init functions can use defer. The defers run when init returns. Nothing surprising — but if init panics, the program may abort before its defers in other init functions get a chance.
Defers on nil pointers¶
The defer registers fine, but when it runs, f.Close() panics because the receiver is nil. Always check the error from acquisition before deferring close.
Argument evaluation captures pointers¶
m := map[string]int{"x": 1}
defer fmt.Println(m) // evaluates m (the map header) now
m["x"] = 99 // mutates the underlying map
// at function exit, deferred prints map[x:99]
The defer captured the map header, not a copy of the contents. Maps and slices are reference types: their headers are evaluated, but the data they point to is shared. If you want a snapshot, copy the map explicitly before the defer.
Defers in returning a closure¶
func returnsCleanup() func() {
f, _ := os.Open("/etc/hostname")
return func() { f.Close() } // caller owns the file now
}
Do not defer f.Close() here — you would close the file before the caller could use it.
Defers across multiple goroutines do not coordinate¶
If you spawn three goroutines, each of them has its own defer stack. A defer in main does not run when goroutine 2 returns. We will return to this in middle.md and senior.md.
Common Mistakes¶
Mistake 1: Deferred close inside a loop¶
Already covered in Example 11. The fix is to extract a helper.
Mistake 2: Ignoring the error from Close on a writer¶
The Close error is dropped. If the disk is full, this code silently loses data.
Mistake 3: Forgetting defer cancel()¶
The cancel is never called. The timer leaks until either the timeout fires or the process exits. go vet catches this.
Mistake 4: Capturing loop variables in deferred closures¶
Go 1.22+ scopes the loop variable per iteration, so this prints 2,1,0. In older Go, it printed 3,3,3 from the shared variable. Either way, do not rely on it; pass i explicitly to a function.
Mistake 5: Deferring on a resource you are about to return¶
// BUG: caller receives a closed file
func openFile(path string) (*os.File, error) {
f, err := os.Open(path)
if err != nil { return nil, err }
defer f.Close()
return f, nil
}
The defer fires before the return value reaches the caller. Either drop the defer (transfer ownership) or do not return the resource.
Mistake 6: Defers after a panic in a different goroutine¶
If main does not block waiting, the program may exit before the goroutine's defer runs. Even when it does run, an unrecovered panic in one goroutine still terminates the whole process.
Mistake 7: Confusing defer cancel() with cleanup of resources¶
defer cancel() releases the context's internal state; it does not close your files, your transactions, or your locks. Each of those needs its own defer.
Common Misconceptions¶
"Defers run in the order I wrote them."
No — LIFO, the reverse order. This is the single most common misconception.
"The arguments to a deferred call are evaluated when it runs."
No — at the moment of the defer statement. The call itself is delayed.
"If a goroutine panics, my main function's defers still run before the program exits."
Yes — but only if main blocks (e.g. via WaitGroup) until the panicking goroutine is done, or if you have a recover in the panicking goroutine.
"I should always defer the close."
Usually yes, but not when the resource is being returned to the caller (transfer of ownership) and not when the resource must be released before the function exits.
"Defer is too slow for production."
No, in the vast majority of cases. Open-coded defers in Go ≥ 1.14 are nearly free. Profile before changing your code.
"context.AfterFunc is just a fancy defer."
No. AfterFunc runs in its own goroutine when the context is cancelled — possibly long after the registering function has returned. It is the right tool for cleanup that must outlive the parent. We will explore it deeply in senior.md.
Tricky Points¶
- The order of side effects matters. If two defers both modify a named return value, the last-pushed defer runs first and sees the value as set by the body; the first-pushed defer runs last and sees the value as set by the second.
- A defer in a deferred function. You can write
defer func() { defer cleanup() }(). The outer closure is registered now; when it runs (at function exit), it registerscleanupon its own (empty) defer stack and immediately exits, runningcleanup. Almost never useful, but legal. return xis actually two steps: assignxto the return values, then run defers, then return. This is why a deferred function can mutate a named return value after thereturnexpression has been evaluated.recoveronly works inside a deferred function. Callingrecover()outside one always returns nil. This is by design.- Method values vs method expressions in defer.
defer obj.Method()evaluatesobj's method-value at defer time. Ifobjis reassigned later, the original method value is still scheduled.
Test¶
A quick self-test. Predict the output before scrolling.
package main
import "fmt"
func main() {
fmt.Println("a")
defer fmt.Println("b")
fmt.Println("c")
defer fmt.Println("d")
fmt.Println("e")
}
Output:
Another:
package main
import "fmt"
func swap() (x int) {
defer func() { x = x * 2 }()
return 5
}
func main() {
fmt.Println(swap())
}
Output: 10. The return 5 sets x = 5; the deferred closure doubles it to 10; the function returns 10.
One more:
package main
import "fmt"
func sneaky() {
i := 0
defer fmt.Println("i =", i)
for i = 0; i < 3; i++ {
}
}
func main() {
sneaky()
}
Output: i = 0. The deferred Println captured the value of i (0) at the defer line. The loop mutates i to 3, but the defer already has its snapshot.
Tricky Questions¶
Q1. What is printed?
A. 2, then 1, then 0. Each defer captures the value of i at its registration. They run in LIFO order, so the last-registered (i=2) runs first.
Q2. What is printed?
A. Nothing visible from f.Close() — the close runs, but its return value is discarded. Then the runtime prints the panic message and the stack trace.
Q3. Why does go vet complain about cancel not being called?
A. Because functions like context.WithCancel return a cancel function that must be invoked to release internal resources. Forgetting it leaks a goroutine and a timer.
Q4. In what order do these print?
func main() {
defer fmt.Println("a")
defer func() {
defer fmt.Println("b")
fmt.Println("c")
}()
fmt.Println("d")
}
A. d, then c, then b, then a. The outer body prints d. Then main's defers pop in LIFO order: the closure runs first, printing c, registering its own defer b, and returning — at which point b fires. Finally main's first defer runs a.
Q5. Is defer f.Close() a leak if f is nil?
A. Not a leak — it's a runtime panic when the defer fires. Always check the error from acquisition before deferring.
Cheat Sheet¶
defer Call(args) register Call with args evaluated NOW
call runs at function exit, LIFO
defer func() { ... }() closure captures variables by reference
named return + defer deferred closure can modify the return
defer cancel() always, for every context.With*
defer mu.Unlock() always, when locked region > 2 lines
defer f.Close() always, after a successful Open
wrap in closure if you care about the error
defer t.Stop() for every NewTimer / NewTicker
DON'T defer in tight loops extract a helper that defers per iteration
DON'T defer on a resource you are about to return to the caller
os.Exit skips defers (rare, but it does happen)
panic runs defers; recover inside a defer
runtime.Goexit runs defers
Self-Assessment Checklist¶
- I can predict the print order of any program with
deferstatements. - I know that defer arguments are evaluated at the defer line, not at exit.
- I can name three resources that should be closed with
defer. - I know why
deferinside a loop is usually a bug, and I can write the fix. - I know how a deferred closure can modify a named return value.
- I can explain the difference between
defer cancel()anddefer f.Close(). - I know that
os.Exitskips defers butpanicruns them. - I can describe what
context.AfterFuncdoes at a one-sentence level. - I can write a
defer trace("name")()helper from scratch. - I have, at least once, run a small program and counted the defers myself to confirm LIFO.
Summary¶
defer is the workhorse of Go cleanup. It registers a function call to run when the surrounding function exits, in LIFO order with respect to other defers, with arguments evaluated at the defer site. It runs on panic. It does not run on os.Exit. It is essentially free in the common case. It interacts with named return values, with closures, with mutexes, with contexts, with timers, and with every resource your code acquires. Cleanup ordering in Go is, at the junior level, exactly the ordering imposed by the defer stack — once you internalise LIFO, half the rules write themselves.
The remaining half — what to do when cleanup must outlive the goroutine that registered it, when context cancellation interacts with deferred shutdown, when errors from cleanup must be propagated through panic and recover, and when the runtime's defer implementation itself becomes a bottleneck — is the subject of the next files. But it all rests on the foundation built here: a function exits, a stack of pending calls unwinds, and your program leaves the world tidier than it found it.
What You Can Build¶
With just the contents of this file you can already write:
- A small file-processing tool that opens, reads, transforms, and writes files with no leaks
- A tiny HTTP client that closes response bodies safely
- A thread-safe counter using
sync.Mutexanddefer mu.Unlock() - A function that prints an "elapsed time" line at exit using the
defer timetrick - A worker function with
defer wg.Done()anddefer recover()that survives panics - A short shell-like utility that wraps every command in
defer cancel()for context cleanup - A test helper that saves and restores an environment variable using
defer
Further Reading¶
- The Go specification, "Defer statements" section: https://go.dev/ref/spec#Defer_statements
- Effective Go, "Defer, Panic, and Recover": https://go.dev/doc/effective_go#defer
- Dave Cheney, Five things that make Go fast — discusses open-coded defers
- The Go blog, "Defer, Panic, and Recover" (2010, still relevant)
- The Go 1.14 release notes, on open-coded defer optimisation
- The Go 1.21 release notes, on
context.AfterFunc golang.org/x/tools/go/analysis/passes/lostcancel— thego vetpass for forgottencancel
Related Topics¶
01-cooperative-vs-force— how a context-cancelled goroutine eventually reaches the function that owns its defers02-partial-cancellation— when only part of a workflow is cancelled, cleanup ordering decides which resources stay alive- The Concurrency overview's
01-goroutines/01-overview— the rule that a goroutine's defers run when that goroutine exits, not when its parent does - Panic / recover patterns (covered later in the Errors-and-Panics track)
context.Context— the source of thecancelyou should always defer
Diagrams & Visual Aids¶
The defer stack¶
function body executes
defer A ┌───┐
defer B │ A │
defer C ├───┤
│ B │
├───┤
│ C │ <- top
└───┘
on return, pop top to bottom:
run C
run B
run A
then return to caller
LIFO matches natural resource nesting¶
acquire DB connection release: 3rd
acquire transaction release: 2nd
acquire prepared statement release: 1st
(LIFO unwinding = correct order)
Defer argument capture¶
defer fmt.Println(x)
^^^^^^^^^^^^^^
x is evaluated NOW
its value is stored in the defer record
later, at function exit:
Println is called with the stored value
not with x's current value
Defer and panic¶
normal exit: body -> defers (LIFO) -> return
panic exit: body -> panic -> defers (LIFO) -> propagate panic
^
recover() here can catch the panic
os.Exit: body -> Exit (no defers, no recover)
Goexit: body -> Goexit -> defers (LIFO) -> goroutine ends
Goroutine-scoped defers¶
parent() --[go child()]--> child()
│ │
│ defer P_done │ defer C_done
│ │
└─ returns: runs P_done └─ returns: runs C_done
independent stacks
defer cancel() vs resource cleanup¶
ctx, cancel := context.WithTimeout(...)
defer cancel() <- releases context's internal timer
f, _ := os.Open(...)
defer f.Close() <- releases the OS file descriptor
These are SEPARATE defers. Each closes a different thing.
A typical request handler¶
handler:
defer span.End() (registered 1st)
defer cancel() (registered 2nd)
defer body.Close() (registered 3rd)
on return:
body.Close() (pop 3rd)
cancel() (pop 2nd)
span.End() (pop 1st, last)
→ spans always close last, capturing everything
Extended Worked Examples¶
The remaining examples in this section take a single resource each and walk through it from acquisition to clean release in real code. They build on Examples 1–15 but spend more time on the reasoning — not just "what is the right pattern," but "why is it the right pattern, and what breaks if you deviate."
Example 16: A complete file copy, with both reads and writes safe¶
package main
import (
"errors"
"fmt"
"io"
"os"
)
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("open src: %w", err)
}
defer func() {
if cerr := in.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close src: %w", cerr)
}
}()
out, err := os.Create(dst)
if err != nil {
return fmt.Errorf("create dst: %w", err)
}
defer func() {
if cerr := out.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close dst: %w", cerr)
}
}()
if _, err = io.Copy(out, in); err != nil {
return fmt.Errorf("copy: %w", err)
}
if err = out.Sync(); err != nil {
return fmt.Errorf("sync: %w", err)
}
return nil
}
func main() {
if err := copyFile("/etc/hostname", "/tmp/hostname.copy"); err != nil {
if errors.Is(err, os.ErrNotExist) {
fmt.Println("source missing")
return
}
fmt.Println("error:", err)
return
}
fmt.Println("copy ok")
}
Notice the structure: - Two acquisitions, each followed immediately by defer. - Both deferred closures use the named return err to propagate close errors only when no earlier error has already been observed. This avoids overwriting a real io.Copy failure with a cosmetic close error. - Sync is called explicitly before the deferred Close to flush kernel buffers — important for any "the file must be on disk before I do X" workflow. - LIFO ordering means out closes before in. The writer is the last resource we touched, so it closes first. Correct.
Example 17: A defer that releases a lock under conditions¶
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.Mutex
items map[string]string
}
func (c *Cache) GetOrLoad(key string, load func() (string, error)) (string, error) {
c.mu.Lock()
if v, ok := c.items[key]; ok {
c.mu.Unlock() // release before doing nothing further
return v, nil
}
c.mu.Unlock() // release during the slow load
v, err := load()
if err != nil {
return "", err
}
c.mu.Lock()
defer c.mu.Unlock()
if existing, ok := c.items[key]; ok {
return existing, nil // someone else loaded it; discard ours
}
c.items[key] = v
return v, nil
}
func main() {
c := &Cache{items: map[string]string{}}
v, _ := c.GetOrLoad("k", func() (string, error) { return "v1", nil })
fmt.Println(v)
}
This example is deliberately not a textbook "lock; defer unlock; do work; return." The function locks, briefly checks state, unlocks, runs a slow load, then locks again. The final lock is paired with a defer Unlock because the rest of the function is short and we know we want to release on every exit path. The earlier Unlock is explicit because we need to release before the slow operation, not on function exit.
Lesson: defer mu.Unlock() is right when the locked region extends to function exit. When it does not, plain Unlock is right.
Example 18: A defer that runs even when an inner goroutine panics¶
package main
import (
"fmt"
"sync"
)
func runWorkers(n int) {
var wg sync.WaitGroup
defer wg.Wait() // ensure we do not leave until all workers exit
for i := 0; i < n; i++ {
wg.Add(1)
i := i
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
fmt.Printf("worker %d recovered from %v\n", i, r)
}
}()
if i == 2 {
panic("boom")
}
fmt.Println("worker", i, "done")
}()
}
}
func main() {
runWorkers(4)
fmt.Println("main done")
}
Two important points: 1. defer wg.Wait() at the top of runWorkers is registered first, so it pops last. That is what we want: the function should wait for workers after doing everything else. 2. Each worker has two defers: wg.Done() (registered first, pops last) and the recover-closure (registered second, pops first). The recover runs before wg.Done, so the recover sees the panic, prints, returns; then wg.Done decrements. If we had swapped the order, wg.Done would run while a panic was still in flight, then the recover would catch it — but the wait group would already be decremented. Same effect for this case, but the principle (cleanup-before-counter-decrement) is good practice.
Example 19: A trace probe that survives panics¶
package main
import (
"fmt"
"time"
)
func trace(name string) func() {
start := time.Now()
fmt.Printf("[ENTER %s]\n", name)
return func() {
fmt.Printf("[EXIT %s after %s]\n", name, time.Since(start))
}
}
func doWork() {
defer trace("doWork")()
time.Sleep(50 * time.Millisecond)
}
func mayCrash() {
defer trace("mayCrash")()
panic("planned")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
doWork()
mayCrash()
}
Output is roughly:
[ENTER doWork]
[EXIT doWork after 50ms]
[ENTER mayCrash]
[EXIT mayCrash after 0s]
recovered: planned
The trace probe prints EXIT for mayCrash before the panic propagates because deferred functions run on panic. This is exactly why defer is the right tool for tracing.
Example 20: Manual cleanup when defer is awkward¶
Sometimes a function must release a resource before it exits — for example, to release a lock while doing slow I/O, or to release a file descriptor that the OS limits. In these cases, defer is still useful for the fallback — the case where you forget to release manually or an error interrupts you. The pattern:
package main
import (
"fmt"
"os"
)
func process(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
closed := false
defer func() {
if !closed {
_ = f.Close()
}
}()
if _, err := f.Stat(); err != nil {
return err
}
// explicit close before doing more work
if err := f.Close(); err != nil {
return err
}
closed = true
// ... more work that does not need the file ...
return nil
}
func main() {
if err := process("/etc/hostname"); err != nil {
fmt.Println(err)
}
}
The closed boolean prevents a double-close. The defer is a safety net: if we forget to set closed = true, or if an error skips that line, the file still gets closed.
Example 21: Nested defers in nested scopes¶
A common pattern in Go's standard library is a "function-with-helper" structure where the helper has its own defer scope:
package main
import (
"fmt"
"os"
)
func main() {
for _, path := range []string{"/etc/hostname", "/etc/hosts"} {
if err := show(path); err != nil {
fmt.Println(path, "→", err)
}
}
}
func show(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
fmt.Printf("%-20s %d bytes\n", path, fi.Size())
return nil
}
Each call to show has its own defer f.Close(). The file closes promptly after each call, regardless of what happens in the next call. This is the canonical fix for the "defer-in-loop" bug.
Example 22: A short-lived context inside a loop¶
package main
import (
"context"
"fmt"
"time"
)
func main() {
for i := 0; i < 3; i++ {
if err := tick(i); err != nil {
fmt.Println("tick", i, "→", err)
}
}
}
func tick(i int) error {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
select {
case <-time.After(50 * time.Millisecond):
fmt.Println("tick", i, "completed")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
Each iteration creates its own context with its own cancel, deferred immediately. When tick returns, cancel fires, releasing the timer. The loop runs three independent rounds with no leaks.
Example 23: A worker that respects its parent context¶
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
go worker(ctx, &wg)
wg.Wait()
}
func worker(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
defer fmt.Println("worker cleanup")
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
fmt.Println("tick")
}
}
}
The worker has three defers, in registration order: wg.Done, fmt.Println("worker cleanup"), ticker.Stop. They pop in reverse: 1. ticker.Stop() — stops the timer goroutine inside time.NewTicker 2. fmt.Println("worker cleanup") — logs that the worker is exiting 3. wg.Done() — signals to main that the worker is fully done
If main's wg.Wait() returned between steps 1 and 3, we would have a race: main would think the worker was done while it was still in the middle of cleanup. By placing wg.Done first in the registration list (= last in execution), we guarantee that "done" really does mean done.
Example 24: Defer with a method on an interface¶
package main
import "fmt"
type resource interface {
Close() error
}
type fakeResource struct{ name string }
func (r *fakeResource) Close() error {
fmt.Println("closing", r.name)
return nil
}
func use(r resource) {
defer r.Close()
fmt.Println("using", r.(*fakeResource).name)
}
func main() {
use(&fakeResource{name: "A"})
}
Output:
r.Close() is a method-value bound to the interface. The defer stores the method-value plus its (zero) arguments. Even if r is reassigned in the body, the bound method-value already captured the original receiver — but be careful: in this snippet r is a parameter, not reassigned. In other code paths you might reassign it; the defer would still call the original.
Example 25: A small "shutdown manager" the manual way¶
Before reaching for context.AfterFunc, it is worth seeing how you would build a small shutdown manager with just plain defer:
package main
import "fmt"
type Shutdown struct {
fns []func()
}
func (s *Shutdown) Add(fn func()) { s.fns = append(s.fns, fn) }
func (s *Shutdown) Run() {
for i := len(s.fns) - 1; i >= 0; i-- {
s.fns[i]()
}
}
func main() {
var s Shutdown
defer s.Run()
s.Add(func() { fmt.Println("close db") })
s.Add(func() { fmt.Println("close redis") })
s.Add(func() { fmt.Println("close logger") })
fmt.Println("doing work")
}
Output:
This is a minimal "registry of cleanup functions" run in LIFO order at process exit. It is equivalent to writing three defers — but it lets you register cleanups from helpers that do not own the top-level function. We will see a more sophisticated version using context.AfterFunc in middle.md.
Example 26: Errors from multiple closes¶
package main
import (
"errors"
"fmt"
)
type closer struct {
name string
err error
}
func (c *closer) Close() error {
fmt.Println("closing", c.name)
return c.err
}
func use() (err error) {
a := &closer{name: "a", err: errors.New("a failed")}
defer func() {
if cerr := a.Close(); cerr != nil {
err = errors.Join(err, cerr)
}
}()
b := &closer{name: "b"}
defer func() {
if cerr := b.Close(); cerr != nil {
err = errors.Join(err, cerr)
}
}()
return nil
}
func main() {
fmt.Println("result:", use())
}
b.Close runs first (LIFO), succeeds. a.Close runs next, returns its error. errors.Join accumulates errors as the defers unwind. The final printed error includes a failed. If both had failed, the result would carry both.
Example 27: Cleanup that must not run twice¶
Some resources are not safe to close twice — for example, certain network connections, channels, or sync.Once-protected handles. A pattern that ensures a single close:
package main
import (
"fmt"
"sync"
)
type Once struct {
closeFn func() error
once sync.Once
err error
}
func (o *Once) Close() error {
o.once.Do(func() { o.err = o.closeFn() })
return o.err
}
func main() {
o := &Once{closeFn: func() error { fmt.Println("real close"); return nil }}
defer o.Close()
defer o.Close() // safe: only one real close
}
The sync.Once ensures the underlying close runs exactly once. The two deferred calls share that guarantee. Useful when you cannot easily prove statically that only one path closes the resource.
Example 28: A panic in cleanup hides the original error¶
package main
import "fmt"
func bad() (err error) {
defer func() {
panic("cleanup panic")
}()
return fmt.Errorf("original error")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
fmt.Println("result:", bad())
}
Output:
The original error from bad is never returned. The cleanup panic replaced the in-flight return. This is one reason to keep cleanup boring: do not call functions that might panic from inside a defer unless you are wrapping them with a recover.
Example 29: A defer registered conditionally¶
package main
import (
"fmt"
"os"
)
func conditional(needFile bool) error {
if needFile {
f, err := os.Open("/etc/hostname")
if err != nil {
return err
}
defer f.Close()
fmt.Println("file opened")
}
fmt.Println("doing work")
return nil
}
func main() {
_ = conditional(true)
_ = conditional(false)
}
The defer is registered only when needFile is true. When the function exits, the defer stack may be empty (the false branch) or contain the close (the true branch). Both behaviours are correct because the defer is a normal statement and obeys normal control flow.
Example 30: Defer with anonymous struct returns¶
package main
import "fmt"
func info() (out struct {
name string
n int
}) {
defer func() { out.n = len(out.name) }()
out.name = "hello"
return
}
func main() {
i := info()
fmt.Println(i.name, i.n)
}
Output:
The deferred closure reads and writes the named return value (out) after the body has set its name. This works for any composite named return, not just plain primitives.
More Coding Patterns¶
Pattern 7: Save/restore environment in tests¶
func TestSomething(t *testing.T) {
old := os.Getenv("DEBUG")
os.Setenv("DEBUG", "1")
defer os.Setenv("DEBUG", old)
// ... test ...
}
Restore the previous value on exit, regardless of whether the test panicked or returned.
Pattern 8: Pair Lock/Unlock with defer, but unlock early when you need to¶
mu.Lock()
defer mu.Unlock()
if cheapCheck() {
return
}
mu.Unlock()
slowWork()
mu.Lock()
// no need to re-defer; the original defer will fire
The deferred Unlock fires on function return. If you Unlock and Lock again inside the body, the deferred call still matches the final state. A double-unlock panics on sync.Mutex; do not let that happen.
Pattern 9: Defer the cancel of every derived context¶
ctx1, cancel1 := context.WithCancel(parent)
defer cancel1()
ctx2, cancel2 := context.WithTimeout(ctx1, time.Second)
defer cancel2()
ctx3, cancel3 := context.WithDeadline(ctx2, deadline)
defer cancel3()
Each cancel is independent. Each is small. Defer all of them.
Pattern 10: Cleanup that runs even if the goroutine is the only reference¶
Even if no other code is waiting for this goroutine, its defer still runs when it exits (return or panic — but not if the whole program exits first).
More Common Mistakes¶
Mistake 8: A defer that captures by closure but the variable was reassigned¶
// BUG
f, _ := os.Open("a.txt")
defer func() { f.Close() }() // captures variable f
f, _ = os.Open("b.txt") // reassigns; "a.txt" is leaked!
defer func() { f.Close() }() // closes "b.txt" twice? no — only once
The first closure captures the variable f. By the time it runs, f points to "b.txt". The first file is leaked. Either copy f into a fresh local before the defer, or use defer f.Close() (the method form, which captures the receiver by value).
Mistake 9: Defer after a check that may return without acquiring¶
// SUBTLE: this is correct.
f, err := os.Open(path)
if err != nil {
return err // no defer; no resource to release
}
defer f.Close() // only registered after acquisition succeeded
But:
Always acquire first, then defer.
Mistake 10: Confusing return values that are set after defer fires¶
// BUG?
func read() error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
if _, err := io.ReadAll(f); err != nil {
return err
}
return nil
}
This is correct, not buggy. The := err inside the if creates a new local err — but the function only returns nil at the end, or the outer err at the top. No defer-related bug here. But contrast with:
// BUG: close error swallowed
func read() error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close() // discards Close's error
_, err = io.ReadAll(f)
return err
}
The close error is dropped. For reads, that is usually fine. For writes, it is not.
Mistake 11: Using defer to release a lock you only briefly held¶
A common over-use: holding a lock for the entire function when you really only need it for two lines. defer mu.Unlock() is convenient but extends the critical section. If the function does slow I/O, your lock is now held during that I/O. Refactor.
Even More Tricky Points¶
- Defer order across receiver and arguments.
defer obj.Method(expensive())evaluatesobjandexpensive()immediately, then schedules the call. Ifexpensive()panics, the defer is never registered — the panic happens at the defer line. returnwith multiple values and defers. With named returns(a, b int), a deferred function can modify either. With unnamed returns, the defer cannot see them.- Defer interaction with
goto. Agotothat jumps out of a block does not skip defers for any function that has not yet returned. Defers run only at function exit, not at block exit. - Defers in deferred panics. If you call
panicinside a deferred function, the original panic value is discarded, and the new one propagates. The next defer in the stack still runs. - Closure traps with
range. Pre-Go-1.22for i, v := range xs { defer fn(v) }capturedvper defer (sincevis the argument, evaluated at defer line). Butfor i, v := range xs { defer func() { fn(v) }() }sharedvacross closures, printing the last value many times. Go 1.22 fixed this by giving each iteration its ownv.
Extended Self-Test¶
package main
import "fmt"
func main() {
defer fmt.Println("1")
func() {
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
}()
defer fmt.Println("5")
fmt.Println("6")
}
Predict the output. Answer:
Reason: the inner anonymous function has its own defer stack, which unwinds when it returns. Then main continues and prints 6. Then main returns and its defers pop: 5, then 1.
Another:
package main
import "fmt"
func first() (n int) {
defer func() { n++ }()
return 10
}
func second() int {
n := 10
defer func() { n++ }()
return n
}
func main() {
fmt.Println(first(), second())
}
Output: 11 10. The named return in first is visible to the defer, which increments it after return 10 assigns. The unnamed return in second is captured by value at the return n statement; the defer increments the local n, but the function has already chosen its return value.
Another tricky case¶
package main
import "fmt"
func tricky() int {
n := 0
defer func() { n++ }()
return n
}
func main() {
fmt.Println(tricky())
}
Output: 0. Unnamed return — the function captures n (value 0) at the return line, then runs the defer (which increments the local n to 1, but the return value is already 0).
Diagrams: Acquisition vs Release Order¶
Code order Defer registration order Run order at exit
─────────────────────────────────────────────────────────────────
open A push A pop C (run close C)
defer close A stack: [A] pop B (run close B)
open B push B pop A (run close A)
defer close B stack: [A, B]
open C push C
defer close C stack: [A, B, C]
LIFO is exactly "close in reverse of open." No bookkeeping needed.
Wrong order Right order
───────────────────────────────────────────
close A close C
close B close B
close C close A
↑ ↑
"close the outer first" "close the inner first"
breaks invariants preserves them
That picture is the whole reason defer is LIFO and not FIFO.
A Longer Worked Story: Building Up a Small Service¶
The rest of this file walks through one extended example — a tiny in-process job runner — to show how cleanup ordering unfolds when you assemble a few moving parts. We add features one at a time, each one introducing one more thing that must be cleaned up.
Step 1: A function that opens a file¶
package main
import (
"fmt"
"io"
"os"
)
func loadJobs(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return splitLines(data), nil
}
func splitLines(b []byte) []string {
var out []string
cur := []byte{}
for _, c := range b {
if c == '\n' {
out = append(out, string(cur))
cur = cur[:0]
} else {
cur = append(cur, c)
}
}
if len(cur) > 0 {
out = append(out, string(cur))
}
return out
}
func main() {
jobs, err := loadJobs("/etc/hostname")
if err != nil {
fmt.Println(err); return
}
fmt.Println("jobs:", jobs)
}
Cleanup: one defer, closes the file. Trivial.
Step 2: Add a mutex around shared state¶
package main
import (
"fmt"
"sync"
)
type Runner struct {
mu sync.Mutex
counter int
}
func (r *Runner) bump() {
r.mu.Lock()
defer r.mu.Unlock()
r.counter++
}
func main() {
r := &Runner{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
r.bump()
}()
}
wg.Wait()
fmt.Println(r.counter)
}
Two kinds of defer here: - defer r.mu.Unlock() inside bump — releases the lock on exit. - defer wg.Done() inside each goroutine — decrements the wait group, even if the goroutine panics.
If we removed defer wg.Done() and r.bump() panicked, wg.Wait() would block forever. The defer is part of why this code is robust.
Step 3: Add a context with cancellation¶
package main
import (
"context"
"fmt"
"sync"
"time"
)
type Runner struct {
mu sync.Mutex
counter int
}
func (r *Runner) bump() {
r.mu.Lock()
defer r.mu.Unlock()
r.counter++
}
func (r *Runner) Run(ctx context.Context) {
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.bump()
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
r := &Runner{}
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); r.Run(ctx) }()
wg.Wait()
fmt.Println("bumps:", r.counter)
}
Cleanup story now: - main defers cancel (releases the timeout's internal goroutine and signals child contexts). - The goroutine defers wg.Done (so main knows when it is safe to exit). - Run defers ticker.Stop (releases the ticker).
If main's timeout fires, ctx.Done() closes, Run sees it and returns, ticker.Stop fires, wg.Done fires, wg.Wait returns, defer cancel fires, main exits. Five defers in three scopes. None of them was registered with knowledge of the others; each one cleans up its own piece.
Step 4: Add a file we write to during work¶
package main
import (
"context"
"fmt"
"os"
"sync"
"time"
)
type Runner struct {
mu sync.Mutex
counter int
out *os.File
}
func (r *Runner) bump() {
r.mu.Lock()
defer r.mu.Unlock()
r.counter++
fmt.Fprintln(r.out, "bump", r.counter)
}
func (r *Runner) Run(ctx context.Context) {
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.bump()
}
}
}
func main() {
f, err := os.Create("/tmp/run.log")
if err != nil {
fmt.Println(err); return
}
defer func() {
if cerr := f.Close(); cerr != nil {
fmt.Println("close error:", cerr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
r := &Runner{out: f}
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); r.Run(ctx) }()
wg.Wait()
fmt.Println("bumps:", r.counter)
}
Order of defers in main: 1. defer f.Close() (registered first → runs last) 2. defer cancel() (registered second → runs second-to-last) 3. (the goroutine's defers run inside the goroutine when it exits, before wg.Wait returns)
When main returns: cancel() runs first, then f.Close(). That order matters: if f.Close() ran first, the goroutine might still be trying to write to f and would hit an "already closed" error.
But wait — the goroutine has already exited by this point, thanks to wg.Wait(). So actually, either order would be safe. But the style matters: we always cancel before closing files because if for some reason wg.Wait() was missing, the cancel-then-close order would at least give the goroutine a chance to notice the cancellation before its writes panicked.
Step 5: Add context.AfterFunc for guaranteed-after-cancel cleanup¶
Imagine we want to log a message every time the context is cancelled, no matter how. defer cannot do this directly — defer runs at function exit, not at cancel. context.AfterFunc is the tool:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
stop := context.AfterFunc(ctx, func() {
fmt.Println("after-func: context cancelled, cause =", context.Cause(ctx))
})
defer stop()
fmt.Println("doing work")
time.Sleep(200 * time.Millisecond)
}
When the timeout fires, AfterFunc runs the callback in a fresh goroutine. The stop is deferred so that if we exit cleanly before cancellation, we deregister the callback. The combination of AfterFunc + defer stop() is the canonical 1.21+ pattern.
We will go deep on AfterFunc in middle.md and senior.md. For now, just appreciate that some cleanup naturally belongs to "after cancel," not "at function exit" — and Go gave us a primitive for it in 1.21.
Step 6: Putting it all together with order analysis¶
Here is the full small service. Note where each defer is registered and predict the unwinding order.
package main
import (
"context"
"fmt"
"os"
"sync"
"time"
)
type Runner struct {
mu sync.Mutex
counter int
out *os.File
}
func (r *Runner) bump() {
r.mu.Lock()
defer r.mu.Unlock()
r.counter++
fmt.Fprintln(r.out, "bump", r.counter)
}
func (r *Runner) Run(ctx context.Context) {
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.bump()
}
}
}
func main() {
f, err := os.Create("/tmp/run.log")
if err != nil { fmt.Println(err); return }
defer func() {
fmt.Println("closing log file")
if cerr := f.Close(); cerr != nil {
fmt.Println("close error:", cerr)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer func() {
fmt.Println("cancel context")
cancel()
}()
stop := context.AfterFunc(ctx, func() {
fmt.Println("after-func: cancelled")
})
defer func() {
fmt.Println("deregister after-func")
stop()
}()
r := &Runner{out: f}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer fmt.Println("goroutine exit")
r.Run(ctx)
}()
fmt.Println("main: waiting")
wg.Wait()
fmt.Println("main: done waiting")
}
Approximate output when running on a 200ms timeout with 20ms ticks:
main: waiting
after-func: cancelled
goroutine exit
main: done waiting
deregister after-func
cancel context
closing log file
Note the LIFO unwinding of main's defers: deregister after-func, then cancel context, then closing log file. They were registered in the opposite order: f.Close, cancel, stop.
This is what cleanup ordering looks like in real Go code. The picture is small but every line earns its place.
Step 7: What would go wrong with the wrong order?¶
What if we registered f.Close() after stop()?
stop := context.AfterFunc(ctx, doStuff)
defer stop()
f, _ := os.Create("/tmp/run.log")
defer f.Close()
Now f.Close() runs first at exit (LIFO), then stop(). If the AfterFunc callback doStuff writes to f, and the callback is still in flight when we run stop, we have already closed f. Result: the callback writes to a closed file.
Why is this even possible? Because stop() does not wait for an in-flight callback to finish. It either deregisters a callback that has not yet started, or it does nothing if the callback has already started. So there is a race window where the callback runs against a closed f.
The fix is exactly what we had: register f.Close() before stop, so stop() runs first at exit. Or, more conservatively, call cancel() explicitly, then sleep/synchronise to make sure the callback finished, then close f. We will explore this in senior.md.
For now, internalise the principle: the order in which you defer cleanup is the order in which it runs in reverse. Choose your acquisition order so that LIFO release matches the dependency direction.
Wrap-up: The Junior's Checklist for Cleanup Ordering¶
You now have:
- The mental model of the per-function defer stack
- The LIFO rule, with the right intuition for why
- Argument-vs-closure capture semantics
- Twelve or so concrete patterns you will actually use in real code
- The three or four most common bugs with their fixes
- A sketch of how
deferinteracts withpanic,recover,context, andAfterFunc - A worked example that grew from one file to a small service with five cleanup layers
The middle file picks up where this one stops: errors from deferred close, cleanup that must survive context cancellation, context.AfterFunc for real, and the wider question of how cleanup ordering interacts with goroutine lifecycles.
Before moving on, make sure you can answer these from memory:
- In
defer fmt.Println(x), when isxevaluated? - In
defer func() { fmt.Println(x) }(), when isxread? - What happens if you
defer f.Close()and thenpanic? - What happens if you
os.Exitfrom inside a function with three pending defers? - Why is
for ... { defer f.Close() }a bug? - Why must
context.WithTimeout's cancel be deferred? - What does
context.AfterFuncdo thatdefercannot?
If those answers come quickly, you are ready for the middle file.
FAQ — Questions Juniors Actually Ask¶
Q. Is defer part of the language or part of the standard library?
It is a language construct, defined in the Go specification. The runtime supplies the implementation, but defer itself is a keyword like for or return.
Q. Can I defer an expression, or only a function call?
Only a function (or method) call. defer 1 + 2 does not compile. The right-hand side of defer must be a syntactic call: defer f(), defer obj.Method(x, y), or defer func() { ... }().
Q. Does the deferred function see changes to the function's local variables made after the defer line?
Closures over local variables: yes (the closure captures the variable, not its value at defer time). Arguments passed to the deferred call: no (arguments are evaluated at the defer line).
Q. Can I recover outside a deferred function?
You can call recover(), but it returns nil if not called from inside a deferred function that is currently unwinding a panic. The intended use is exclusively from inside a defer.
Q. Does defer have any heap cost?
In Go ≥ 1.14, simple defers that the compiler can prove are not inside a loop are open-coded: they compile to a small block of conditional cleanup at the function's exit point with no heap allocation. Defers that the compiler cannot open-code allocate a small _defer record on the goroutine's defer chain. Allocation is roughly one struct per defer.
Q. Is defer slower than calling cleanup manually?
In microbenchmarks, manual cleanup is faster. In real programs, the difference is rarely measurable. Use defer for correctness; profile before optimising.
Q. What if my deferred function takes a long time?
It still runs. The function does not return to its caller until all defers finish. If your defer does I/O that takes seconds, your function takes seconds to return. This is rarely what you want.
Q. Can I cancel a defer once it is registered?
Not directly. You can make the deferred call a no-op by setting a flag that the closure checks:
But this is rarely useful.
Q. Why do all my deferred Closes silently fail in production?
Probably because you wrote defer f.Close() without checking the error. For writes, swap in the named-return pattern:
Q. Should I always use the named-return pattern?
For functions that write and then close, yes. For functions that only read, the cost of the pattern outweighs the benefit (read-close almost never fails meaningfully).
Q. Why does go vet warn me about a missing cancel?
Because functions like context.WithCancel and context.WithTimeout return a cancel function that you must invoke. Forgetting it leaks the context's internal goroutine and timer. The vet check is called lostcancel.
Q. Can I defer in main?
Yes. main's defers run before the program exits — unless you call os.Exit, in which case they do not. This is one reason to avoid os.Exit in main and let return handle it.
Q. What happens if the deferred function panics during another panic?
The new panic replaces the old one. The old panic value is lost (unless something recover-ed it earlier). The next defer in the stack still runs with the new panic in flight.
Q. Is defer thread-safe?
The defer mechanism is per-goroutine, so there is no sharing. But the function you defer can have its own thread-safety issues — for example, defer mu.Unlock() is safe only if mu was locked by this goroutine.
Q. Can I defer a method on a struct field?
Yes: defer obj.field.Method(). Be aware that obj and obj.field are evaluated at the defer line. If obj.field is later reassigned, the defer still calls the original field's method.
Q. Does defer work with generic functions?
Yes. From the language's point of view, the deferred call is just a function call. Type parameters are erased at the defer site.
Q. Should I defer in a function that already has a recover?
The defer registers normally; the recover runs inside it. Common pattern:
The function's other defers still run normally — recover catches the panic, the function exits normally, the remaining defers fire on the way out.
Q. Does the order of defers matter for performance?
No. The defer chain is a linked list (or, for open-coded defers, a sequence of branches). Both pop quickly in LIFO order. The order matters only for correctness: which resource you release first.
Q. Can a defer modify an unnamed return value?
No. Unnamed returns are captured at the return statement; deferred functions cannot reach the values that have already been moved into the return slot. If you want a defer to influence the return, name your returns.
Q. What happens if I defer inside init?
init's defers run when init returns, just like any function. There is nothing magical.
Q. Is there a way to see the defer chain at runtime?
Not portably. The runtime keeps the chain internally, but Go does not expose it. Debuggers like Delve can show it. In production, you cannot programmatically enumerate pending defers.
Q. Can defer leak memory?
A heap-allocated defer record costs a few dozen bytes. They are cleaned up when the function returns. A defer-in-loop bug can leak memory in the sense that many records accumulate until the enclosing function returns — at which point they all clear at once. The bigger leak is usually file descriptors or goroutines, not the defer records themselves.
Q. What is the maximum number of defers in one function?
There is no hard limit. The runtime can chain arbitrarily many. The compiler only open-codes the first eight (in Go 1.14+); the rest fall back to heap allocation. So eight is the "free" budget; beyond that you pay per defer.
Q. Should I be afraid of nested defers in a closure?
Not at all. The defer mechanism handles them correctly: the closure has its own defer scope, which unwinds when the closure returns. Just remember that the closure's defers run when the closure returns, not when the outer function returns.
Q. What if I want a cleanup to run before every return, but only conditionally?
Use a function:
The defer calls whatever cleanup happens to be at the moment it runs (because cleanup is captured by the closure — well, by the deferred call's receiver, which is the variable itself).
Actually wait: defer cleanup() evaluates cleanup at the defer line. So the original function is what runs, not a later reassignment. To get late-binding, wrap it: defer func() { cleanup() }(). Now the closure looks up cleanup at exit and runs whatever it points to.
This subtlety bites people. Remember: - defer f() — f is looked up now, called later. - defer func() { f() }() — closure looked up later; f looked up inside the closure when it runs.
The junior story ends here. The middle file picks up the deeper questions: errors from cleanup, AfterFunc semantics, and cleanup that must survive cancellation.
One More Practice Drill¶
Read the code below carefully, predict the output, then run it.
package main
import "fmt"
func a() (s string) {
defer func() { s = s + "/a-defer" }()
s = "a"
return
}
func b() string {
s := "b"
defer func() { s = s + "/b-defer" }()
return s
}
func c() (x int) {
defer func() { x++ }()
defer func() { x *= 2 }()
x = 3
return
}
func main() {
fmt.Println(a())
fmt.Println(b())
fmt.Println(c())
}
Expected output:
Reasoning: - a: named return s starts as "a" after s = "a". return assigns s = "a" to the return slot (no-op). Defer runs: s becomes "a/a-defer". Function returns "a/a-defer". - b: unnamed return. return s captures the value "b". Defer modifies the local s, but the return slot already has "b". - c: named return x. x = 3. return is a no-op. Defers pop LIFO: first x *= 2 (x=6), then x++ (x=7). Function returns 7.
If you got all three right without running the code, you have internalised the rules. Move on to middle.md.
Quick Reference Card (Printable)¶
DEFER CHEAT CARD
================
defer f() evaluate f and args NOW, run later
defer func() { ... }() closure captures variables by ref
LIFO order at function exit
runs on return, runs on panic, NOT on os.Exit
runs in the goroutine that registered it
named returns: defer can read/write them
unnamed returns: defer cannot reach them
ALWAYS
defer cancel() for every context.With*
defer mu.Unlock() after mu.Lock() if region > 2 lines
defer f.Close() after a successful Open
defer t.Stop() for every NewTicker / NewTimer
defer wg.Done() at top of every goroutine using wg
NEVER
defer in a tight loop extract a helper
defer on a returned resource caller owns it
defer Close without err for writers; use named-return pattern
CONTEXT
cancel sends a signal, not a cleanup
defer cancel() releases context's timer
context.AfterFunc (1.21+) runs in a new goroutine on cancel
Tape this to your monitor for a week. After a week, you will not need it.