Defer
Defer schedules a function call to run when the enclosing function returns - LIFO execution order and idiomatic resource cleanup.
Defer schedules a function call to be executed when the enclosing function returns (normally or via panic). Deferred calls execute in LIFO order - last defer registered is first to run. Defer is Go's idiomatic way to pair resource acquisition with cleanup (open/close, lock/unlock, begin/commit).
Defer statements execute after the current function returns, in reverse order. Arguments to the deferred function are evaluated immediately, but the function itself is not called until the return.
package main
import (
"fmt"
)
func main() {
defer fmt.Println("third")
defer fmt.Println("second")
defer fmt.Println("first")
fmt.Println("main")
}
// Output:
// main
// first
// second
// thirdDefer is commonly used to clean up resources - close files, release locks, commit or rollback transactions. Pairing the cleanup (defer) immediately after acquisition ensures cleanup runs even on early return or panic.
package main
import (
"fmt"
"os"
)
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // cleanup runs even if we return early
// Read file content...
fmt.Println("reading", path)
return nil
}
func transferMoney(from, to string, amount int) error {
// Begin transaction
tx, err := beginTx()
if err != nil {
return err
}
defer tx.Rollback() // rollback on any error
if err := debit(tx, from, amount); err != nil {
return err // defer tx.Rollback() runs
}
if err := credit(tx, to, amount); err != nil {
return err // defer tx.Rollback() runs
}
return tx.Commit() // replaces the rollback
}
// Simplified mock functions
func beginTx() (*Tx, error) { return &Tx{}, nil }
type Tx struct{}
func (t *Tx) Rollback() error { return nil }
func (t *Tx) Commit() error { return nil }
func debit(*Tx, string, int) error { return nil }
func credit(*Tx, string, int) error { return nil }Defer with named return values: the deferred function can modify named return values. This is useful for wrapping results or converting panics to errors, but can hurt readability in long functions - use it sparingly for cleanup, not for control flow.
package main
import (
"fmt"
)
func processWithLogging() (result string, err error) {
defer func() {
if err != nil {
fmt.Println("error:", err)
}
}()
// Simulate work
result = "success"
return
}
func double(x int) (result int) {
defer func() {
result *= 2 // modify the named return value
}()
result = x
return // returns x * 2
}
func main() {
r, _ := processWithLogging()
fmt.Println(r) // "success"
fmt.Println(double(5)) // 10
}Avoid defer inside loops over many items - each defer allocates and the cleanup does not run until the enclosing function returns. Extract the loop into a separate function to ensure cleanup runs after each iteration.
package main
import (
"fmt"
"os"
)
// BAD: defer inside loop - all file handles accumulate until processMany returns
func processMany_Bad(paths []string) error {
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // cleanup happens after ALL files are opened
// process file...
}
return nil
}
// GOOD: extract into a separate function - cleanup runs after each iteration
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // cleanup runs immediately after processOne returns
// process file...
return nil
}
func processMany(paths []string) error {
for _, path := range paths {
if err := processOne(path); err != nil {
return err
}
}
return nil
}
func main() {
processMany([]string{})
}In production
Defer is the idiomatic way to ensure cleanup code runs - it pairs acquisition and cleanup in one place, visible in the code, and guarantees execution even on panic or early return. Always call defer immediately after a successful acquisition; missing this pattern is a common source of resource leaks. The trade-off: deferred functions allocate and add overhead, so avoid defer inside tight loops over many items - extract into a helper function. Go 1.22 reduces defer allocation overhead significantly, but the pattern of extracting loops remains good practice for readability. Use named return values to convert panics to errors at package boundaries, but keep the pattern simple - if the defer is large or complex, it belongs in a separate function.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free