Go/
Lesson

Go separates "something went wrong but we can handle it" (error returns) from "the program is broken" (panic). Most languages use exceptions for both. This distinction is fundamental to Go, and AI regularly gets it wrong -- panicking for network timeouts, missing defer for cleanup, or putting recover in the wrong place.

defer: guaranteed cleanup

defer schedules a function call to run when the enclosing function returns. Not when the block ends. Not when the scopeWhat is scope?The area of your code where a variable is accessible; variables declared inside a function or block are invisible outside it. closes. When the function returns.

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // runs when readFile returns

    data := make([]byte, 100)
    _, err = file.Read(data)
    return err
}

The placement pattern matters: acquire the resource, check for error, then immediately defer the cleanup. This ensures the cleanup is registered right after successful acquisition.

AI pitfall
AI sometimes places defer file.Close() before the error check on os.Open. If os.Open fails, file is nil, and defer file.Close() will panic with a nil pointer dereference when the function returns. Always verify: is defer placed after the error check?
02

LIFO order: last deferred runs first

Multiple defers execute in reverse order -- like a stack:

func main() {
    defer fmt.Println("First deferred")
    defer fmt.Println("Second deferred")
    defer fmt.Println("Third deferred")
    fmt.Println("Doing work...")
}
// Output:
// Doing work...
// Third deferred
// Second deferred
// First deferred

This is deliberate. When you acquire nested resources (open file, start transactionWhat is transaction?A group of database operations that either all succeed together or all fail together, preventing partial updates., acquire lock), LIFO ensures they are released in the correct reverse order:

func doWork() {
    a := acquireA()
    defer releaseA(a) // released third

    b := acquireB()
    defer releaseB(b) // released second

    c := acquireC()
    defer releaseC(c) // released first
}
03

Defer argument evaluation -- the sneaky trap

Deferred function arguments are evaluated immediately when the defer statement executes, not when the deferred function runs:

func main() {
    x := 10
    defer fmt.Println(x) // captures x=10 right now
    x = 20
    fmt.Println(x) // prints 20
}
// Output:
// 20
// 10  <-- defer captured the value at defer time
AI pitfall
This is one of the most common defer bugs AI generates. AI writes defer log.Printf("took %v", time.Since(start)) expecting it to measure elapsed time. But time.Since(start) is evaluated at defer time (immediately), not at function return. The fix is a closure: defer func() { log.Printf("took %v", time.Since(start)) }(). If you see a defer with a computation in its arguments, check whether it should be a closure instead.

The closureWhat is closure?A function that remembers variables from the surrounding code where it was created, even after that surrounding code has finished running. fix

// Wrong: evaluates time.Since(start) immediately
defer log.Printf("took %v", time.Since(start))

// Right: closure evaluates time.Since(start) at return time
defer func() {
    log.Printf("took %v", time.Since(start))
}()
04

Defer in loops -- resource leak factory

AI loves generating defer inside loops. This is almost always wrong:

// BAD: all files stay open until the function returns
for _, filename := range files {
    f, err := os.Open(filename)
    if err != nil {
        continue
    }
    defer f.Close() // defers accumulate!
    processFile(f)
}

If you have 10,000 files, all 10,000 file handles stay open until the function returns. The fix is wrapping the body in a function:

// GOOD: each file closes after processing
for _, filename := range files {
    func() {
        f, err := os.Open(filename)
        if err != nil {
            return
        }
        defer f.Close()
        processFile(f)
    }()
}
AI pitfall
AI generates defer-in-loop patterns constantly. Every time you see defer inside a for loop in AI output, stop and check. Ask: "Will these deferred calls accumulate?" If yes, wrap in a function or extract to a helper.
05

panic: the nuclear option

panic immediately stops normal execution, runs deferred functions, then crashes the program. It is for programmer bugs, not for expected errors.

ScenarioUse panic?What to do instead
File not foundNoreturn nil, err
Network timeoutNoreturn nil, err
Invalid user inputNoreturn nil, fmt.Errorf(...)
Nil pointer you forgot to checkHappens automaticallyFix the nil check
Index out of boundsHappens automaticallyFix the bounds check
Impossible state (should never happen)Maybepanic("unreachable")
// Good: return an error for expected failures
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

// Bad: panic for something the caller should handle
func dividePanic(a, b float64) float64 {
    if b == 0 {
        panic("division by zero") // don't do this
    }
    return a / b
}
AI pitfall
AI trained on Python and Java defaults to exception-style thinking. It generates panic("invalid input") where Go convention demands returning an error. If you see panic with a user-facing message, it is almost certainly wrong. Panics are for developer mistakes, not user mistakes.
06

recover: catching panics

recover stops a panic in progress and returns the panic value. It only works inside a deferred function:

func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Recovered: %v\n", r)
        }
    }()

    panic("something broke")
    fmt.Println("This line never executes")
}

func main() {
    safeCall()
    fmt.Println("Program continues") // this runs
}

Why recover only works in deferred functions

When a panic occurs, Go unwinds the call stackWhat is call stack?The internal mechanism that tracks which function is currently executing and which called it - visible in error stack traces and browser DevTools., running deferred functions along the way. recover intercepts this unwinding. If called outside a deferred function, there is no panic to catch, so it returns nil.

AI pitfall
AI sometimes puts recover() in regular code (not inside a defer). This compiles but does nothing -- recover silently returns nil outside of a deferred function call. It also sometimes places the defer recover() in the wrong function -- recover only catches panics in the function where the defer is registered, not in child function calls (unless the child's panic propagates up).
07

The real-world pattern: HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. server recovery

This is the most common legitimate use of recover in production Go:

func handler(w http.ResponseWriter, r *http.Request) {
    defer func() {
        if rec := recover(); rec != nil {
            log.Printf("panic in handler: %v", rec)
            http.Error(w, "Internal Server Error", 500)
        }
    }()

    // handler logic that might panic
    processRequest(w, r)
}

HTTP servers use this to prevent one panicking request handler from crashing the entire server. The standard libraryWhat is standard library?A collection of ready-made tools that come built into a language - no install required. Covers common tasks like reading files or making web requests.'s net/http actually does this internally, but explicit recovery lets you log the panic.

08

Quick reference

KeywordPurposeWhen it runsKey rule
deferCleanupWhen enclosing function returnsArguments evaluated immediately
panicCrashImmediatelyUse only for programmer errors
recoverCatch panicInside deferred functions onlyReturns nil if no panic
09

Common defer uses AI generates correctly

// File cleanup
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()

// Mutex unlock
mu.Lock()
defer mu.Unlock()

// HTTP response body
resp, err := http.Get(url)
if err != nil { return err }
defer resp.Body.Close()

// Database transaction
tx, err := db.Begin()
if err != nil { return err }
defer tx.Rollback() // no-op if committed

These are all correct and idiomatic. The pattern is always: acquire, check error, defer release.