Go Defer

Delaying function execution until function returns

โฐ What is Go Defer?

Defer schedules function calls to run after the surrounding function returns. It's perfect for cleanup tasks like closing files, unlocking mutexes, or releasing resources, ensuring they execute regardless of how the function exits.


func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // Always closes, even if error occurs
    
    // Process file...
    data, err := io.ReadAll(file)
    return err
}
                                    

Behavior:

file.Close() runs automatically when processFile returns

Defer Concepts

Key Defer Concepts

๐Ÿ”„

LIFO Order

Last deferred function runs first

defer fmt.Println("1")
defer fmt.Println("2") // Runs first
๐Ÿ“‹

Argument Evaluation

Arguments evaluated when defer is called

x := 5
defer fmt.Println(x) // Prints 5, not later value
๐Ÿงน

Cleanup

Perfect for resource cleanup

defer file.Close()
defer mutex.Unlock()
๐Ÿ›ก๏ธ

Panic Safety

Runs even during panics

defer recover()
panic("error")

๐Ÿ”น Basic Defer Usage

Understanding how defer works with execution order:

func demonstrateDefer() {
    fmt.Println("Start of function")
    
    defer fmt.Println("Deferred 1")
    defer fmt.Println("Deferred 2")
    defer fmt.Println("Deferred 3")
    
    fmt.Println("Middle of function")
    
    if true {
        defer fmt.Println("Deferred 4 (in if block)")
        fmt.Println("Inside if block")
    }
    
    fmt.Println("End of function")
}

func main() {
    demonstrateDefer()
    fmt.Println("After function call")
}

Output:

Start of function
Middle of function
Inside if block
End of function
Deferred 4 (in if block)
Deferred 3
Deferred 2
Deferred 1
After function call

๐Ÿ”น Resource Management

Using defer for proper resource cleanup:

import (
    "fmt"
    "os"
    "sync"
)

func copyFile(src, dst string) error {
    // Open source file
    srcFile, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("cannot open source file: %w", err)
    }
    defer srcFile.Close() // Ensure source file is closed
    
    // Create destination file
    dstFile, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("cannot create destination file: %w", err)
    }
    defer dstFile.Close() // Ensure destination file is closed
    
    // Copy data
    _, err = io.Copy(dstFile, srcFile)
    if err != nil {
        return fmt.Errorf("copy failed: %w", err)
    }
    
    return nil
}

func safeCounter() {
    var mu sync.Mutex
    var count int
    
    increment := func() {
        mu.Lock()
        defer mu.Unlock() // Always unlock, even if panic occurs
        
        count++
        fmt.Printf("Count: %d\n", count)
        
        // Even if this panics, mutex will be unlocked
        if count == 3 {
            panic("demonstration panic")
        }
    }
    
    // Recover from panic to show defer still works
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered from: %v\n", r)
            fmt.Printf("Final count: %d\n", count)
        }
    }()
    
    for i := 0; i < 5; i++ {
        increment()
    }
}

func main() {
    safeCounter()
}

Output:

Count: 1
Count: 2
Count: 3
Recovered from: demonstration panic
Final count: 3

๐Ÿ”น Defer with Function Arguments

Understanding how defer evaluates arguments:

func argumentEvaluation() {
    x := 1
    
    // Arguments are evaluated immediately
    defer fmt.Printf("Immediate evaluation: x = %d\n", x)
    
    // Use closure to capture variable by reference
    defer func() {
        fmt.Printf("Closure evaluation: x = %d\n", x)
    }()
    
    x = 2
    fmt.Printf("During function: x = %d\n", x)
    x = 3
}

func namedReturns() (result string) {
    defer func() {
        // Can modify named return values
        result = "modified by defer: " + result
    }()
    
    return "original value"
}

func deferWithLoop() {
    fmt.Println("Defer in loop - common mistake:")
    
    // This creates 5 separate deferred calls
    for i := 0; i < 5; i++ {
        defer fmt.Printf("Loop defer %d\n", i)
    }
    
    fmt.Println("Loop completed")
}

func main() {
    fmt.Println("=== Argument Evaluation ===")
    argumentEvaluation()
    
    fmt.Println("\n=== Named Returns ===")
    result := namedReturns()
    fmt.Printf("Returned: %s\n", result)
    
    fmt.Println("\n=== Defer in Loop ===")
    deferWithLoop()
}

Output:

=== Argument Evaluation ===
During function: x = 2
Closure evaluation: x = 3
Immediate evaluation: x = 1

=== Named Returns ===
Returned: modified by defer: original value

=== Defer in Loop ===
Loop completed
Loop defer 4
Loop defer 3
Loop defer 2
Loop defer 1
Loop defer 0

๐Ÿ”น Common Defer Patterns

Practical patterns for using defer effectively:

// Pattern 1: Timing functions
func timeFunction(name string) func() {
    start := time.Now()
    fmt.Printf("Starting %s\n", name)
    
    return func() {
        fmt.Printf("%s took %v\n", name, time.Since(start))
    }
}

func expensiveOperation() {
    defer timeFunction("expensiveOperation")()
    
    // Simulate work
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Doing expensive work...")
}

// Pattern 2: State restoration
func temporaryChange() {
    originalValue := globalConfig.Debug
    globalConfig.Debug = true
    defer func() {
        globalConfig.Debug = originalValue
    }()
    
    fmt.Printf("Debug mode: %t\n", globalConfig.Debug)
    // Do debug work...
}

// Pattern 3: Error handling with cleanup
func processWithCleanup() error {
    resource := acquireResource()
    defer func() {
        if err := resource.Release(); err != nil {
            fmt.Printf("Warning: failed to release resource: %v\n", err)
        }
    }()
    
    if err := resource.Process(); err != nil {
        return fmt.Errorf("processing failed: %w", err)
    }
    
    return nil
}

// Pattern 4: Panic recovery with logging
func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Operation panicked: %v\n", r)
            // Log stack trace, send alerts, etc.
        }
    }()
    
    // Potentially dangerous operation
    riskyWork()
}

type Config struct {
    Debug bool
}

var globalConfig = &Config{Debug: false}

type Resource struct{}

func acquireResource() *Resource { return &Resource{} }
func (r *Resource) Process() error { return nil }
func (r *Resource) Release() error { return nil }
func riskyWork() { /* some risky operation */ }

func main() {
    fmt.Println("=== Timing Pattern ===")
    expensiveOperation()
    
    fmt.Println("\n=== State Restoration ===")
    fmt.Printf("Before: Debug = %t\n", globalConfig.Debug)
    temporaryChange()
    fmt.Printf("After: Debug = %t\n", globalConfig.Debug)
}

Output:

=== Timing Pattern ===
Starting expensiveOperation
Doing expensive work...
expensiveOperation took 100ms

=== State Restoration ===
Before: Debug = false
Debug mode: true
After: Debug = false

๐Ÿ”น Defer Best Practices

Guidelines for using defer effectively:

Do:

  • Use for cleanup: Close files, unlock mutexes, release resources
  • Place near acquisition: Put defer right after acquiring resource
  • Use closures for variables: When you need current variable values
  • Handle errors in defer: Check and log cleanup errors

Don't:

  • Defer in loops: Creates many deferred calls (use separate function)
  • Ignore defer errors: Always handle cleanup errors appropriately
  • Rely on execution order: Between different functions
  • Use for performance-critical paths: Defer has small overhead
// Good: Defer right after acquisition
func goodFileHandling(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // Right after opening
    
    // Use file...
    return nil
}

// Bad: Defer in loop
func badLoopDefer(files []string) {
    for _, filename := range files {
        file, _ := os.Open(filename)
        defer file.Close() // Creates many deferred calls!
        // Process file...
    }
}

// Good: Separate function for loop
func goodLoopDefer(files []string) {
    for _, filename := range files {
        processFile(filename)
    }
}

func processFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        return
    }
    defer file.Close() // Only one defer per file
    
    // Process file...
}

๐Ÿง  Test Your Knowledge

In what order do deferred functions execute?