Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Control Structures

Go has fewer control structures than languages like C or Java, but they are versatile. This is intentional: Go’s philosophy is that fewer, more powerful constructs lead to more readable code than many specialized ones. There is only one loop keyword (for), no ternary operator, and switch does not fall through by default.

If / Else

The syntax is similar to C, but parentheses () are not required around the condition, and braces {} are mandatory.
x := 10

if x > 5 {
    fmt.Println("x is big")
} else {
    fmt.Println("x is small")
}

If with Short Statement

You can execute a short statement before the condition. Variables declared here are scoped to the if block. This is one of Go’s most idiomatic patterns — you will see it everywhere in production code, especially for error handling:
// The classic Go error-handling idiom:
if err := doSomething(); err != nil {
    return fmt.Errorf("doSomething failed: %w", err)
}
// err is not accessible here -- it is scoped to the if block

// Also useful for computations:
if v := math.Pow(x, n); v < lim {
    return v
} else {
    fmt.Printf("%g >= %g\n", v, lim)
}
// v is not available here
This scoping is valuable because it keeps variables confined to where they are meaningful, preventing accidental reuse of a stale err from an earlier call.

For Loops

Go has only one looping construct: the for loop. It can be used in three ways.

1. Standard C-style Loop

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

2. While-style Loop

If you omit the init and post statements, it acts like a while loop.
sum := 1
for sum < 1000 {
    sum += sum
}

3. Infinite Loop

If you omit the condition, it loops forever.
for {
    // do something forever
    if condition {
        break // exit loop
    }
}

Switch Statements

Go’s switch is more powerful than in C.
  • No break needed (it’s implicit).
  • Cases don’t need to be constants or integers.
import "runtime"

switch os := runtime.GOOS; os {
case "darwin":
    fmt.Println("OS X.")
case "linux":
    fmt.Println("Linux.")
default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.\n", os)
}

Switch with no condition

This is a clean way to write long if-then-else chains.
t := time.Now()
switch {
case t.Hour() < 12:
    fmt.Println("Good morning!")
case t.Hour() < 17:
    fmt.Println("Good afternoon.")
default:
    fmt.Println("Good evening.")
}

Defer

A defer statement defers the execution of a function until the surrounding function returns. The deferred call’s arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.
func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}
// Output:
// hello
// world

Stacking Defers

Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.
func main() {
    fmt.Println("counting")

    for i := 0; i < 4; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}
// Output:
// counting
// done
// 3
// 2
// 1
// 0
Think of defer as a “cleanup reminder” you attach the moment you acquire a resource. It is the Go equivalent of finally blocks in Java or context managers in Python, but more flexible because you can defer any function call. This is extremely useful for resource cleanup (closing files, unlocking mutexes).
f, err := os.Open("filename")
if err != nil {
    return err
}
defer f.Close() // Will be called when function exits, no matter how it exits

// Idiomatic mutex pattern:
mu.Lock()
defer mu.Unlock()
// ... critical section is protected even if a panic occurs
Pitfall — Defer in Loops: defer executes when the function returns, not when the loop iteration ends. This means deferred calls inside a loop pile up until the function exits, which can leak resources:
// WRONG: All files stay open until the function returns
for _, name := range filenames {
    f, _ := os.Open(name)
    defer f.Close() // These all pile up!
}

// RIGHT: Extract to a helper function so defer runs per iteration
for _, name := range filenames {
    if err := processFile(name); err != nil {
        return err
    }
}

func processFile(name string) error {
    f, err := os.Open(name)
    if err != nil {
        return err
    }
    defer f.Close() // Runs when processFile returns -- correct!
    // ... process file
    return nil
}
Pitfall — Defer with Error Returns: When deferring a Close() that can return an error, you silently discard the error. For writes, this can mean data loss:
// Dangerous for writes -- Close() might flush a buffer and fail
defer f.Close()

// Safer pattern for writable files:
defer func() {
    if cerr := f.Close(); cerr != nil && err == nil {
        err = cerr // Capture the close error in the named return
    }
}()

Interview Deep-Dive

Strong Answer:
  • Defer has three key rules. First, arguments to the deferred function are evaluated immediately at the point where defer is called, not when the deferred function actually executes. So defer fmt.Println(x) captures the current value of x. Second, deferred functions execute when the surrounding function returns — not when the block or loop ends. Third, multiple defers stack in LIFO (last-in-first-out) order, so the last defer registered runs first.
  • The argument-evaluation rule catches people regularly. If you want the deferred function to use the value of a variable at the time it executes (not at the time of the defer call), you need to either use a closure that captures the variable by reference, or use a named return value that the deferred closure can access.
  • The “runs when the function returns” rule is why defer in a loop is dangerous: if you defer f.Close() inside a loop that opens 10,000 files, all 10,000 files stay open until the function returns. The fix is to extract the loop body into a helper function so each iteration’s defer runs at the end of the helper.
  • Defer also executes during a panic, which is why defer mu.Unlock() after mu.Lock() is safe even if the critical section panics. This is the mechanism that makes recover() work — it can only be called from a deferred function because deferred functions are the only code that runs during stack unwinding.
Follow-up: Show me the subtle bug in this code: for i := 0; i < 5; i++ { defer fmt.Println(i) }. What prints and why?It prints 4, 3, 2, 1, 0 — not 5, 5, 5, 5, 5. Because defer arguments are evaluated immediately, each defer fmt.Println(i) captures the current value of i at that iteration. And because defers execute in LIFO order, they print in reverse. This is actually correct behavior and not a bug. The real bug people expect is the closure variant: for i := 0; i < 5; i++ { defer func() { fmt.Println(i) }() }. In Go versions before 1.22, this would print 5, 5, 5, 5, 5 because the closure captures the variable i by reference, not its value, and by the time the deferred closures run, the loop has completed and i is 5. The fix for pre-1.22 code is i := i shadowing inside the loop. Go 1.22+ changed loop variable semantics so each iteration gets its own variable.
Strong Answer:
  • The Go designers believed that having multiple loop keywords (while, do-while, for, foreach) adds cognitive overhead without adding capability. Every loop can be expressed with for in three forms: the C-style for init; condition; post, the while-style for condition, and the infinite for with explicit break. The range clause handles iteration over slices, maps, channels, and strings.
  • This is consistent with Go’s broader philosophy of “one obvious way to do things.” By having a single loop keyword, you eliminate debates about which loop type to use, and code reviews focus on logic rather than style. It also means every Go developer reads loops in the same way.
  • For a do-while (execute at least once, then check condition), you use for { body; if !condition { break } }. It is slightly more verbose but perfectly clear.
  • One pattern that is unique to Go’s for range on channels: for msg := range ch reads from the channel until it is closed, which is the idiomatic way to consume a channel in a consumer goroutine. This ties the loop construct directly to Go’s concurrency model.
Follow-up: Go’s switch statement does not fall through by default, unlike C. Why was this chosen, and when would you use the fallthrough keyword?In C and C++, forgetting a break in a switch case is one of the most common sources of bugs — execution “falls through” to the next case silently. Go inverts the default: each case breaks automatically, and you must explicitly say fallthrough if you want to continue to the next case. This eliminates an entire class of bugs. The fallthrough keyword is rarely used in practice — most Go codebases never use it. The few legitimate uses are when you have multiple cases that share the same setup logic before diverging, but even then, calling a shared function is usually cleaner. If you see fallthrough in a code review, it is worth scrutinizing carefully because it is almost always a sign that the logic could be restructured more clearly.
Strong Answer:
  • The pattern is to acquire each resource, check for error immediately, and defer its cleanup right after successful acquisition. This creates a clear “acquire-defer-use” rhythm:
  • Open file, check error, defer f.Close(). Query database, check error, defer rows.Close(). Make HTTP request, check error, defer resp.Body.Close(). If any step fails, the function returns early, and all previously deferred cleanups run automatically.
  • The key insight is that defers only run for resources that were successfully acquired. If the database query fails, the file’s defer still runs (it was registered before the failure), but no HTTP defer exists because we never reached that point.
  • For writable files, I would use a closure defer that captures the named return error: defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr } }(). This ensures that a flush error during Close is not silently swallowed.
  • For the database rows, I would also check rows.Err() after the iteration loop, because rows.Next() can fail silently (it returns false on both completion and error).
  • One production-grade addition: if the HTTP response body is not fully read, the underlying TCP connection cannot be reused by the connection pool. So before closing, I would add io.Copy(io.Discard, resp.Body) to drain any remaining bytes, or use http.MaxBytesReader to limit the body size.
Follow-up: What happens if a panic occurs between acquiring a resource and deferring its cleanup?If a panic occurs between f, err := os.Open(name) and defer f.Close(), the file handle is leaked because the defer was never registered. In practice, this is extremely rare because the defer call is the very next line. But in adversarial or safety-critical code, you could use a pattern where you register a cleanup defer first and then acquire the resource: var f *os.File; defer func() { if f != nil { f.Close() } }(); f, err = os.Open(name). However, this is over-engineering for most cases. The real defense is that panics in Go should be rare — they indicate programming errors (nil dereference, out-of-bounds access), not expected failure conditions. If you are dealing with a codebase that panics frequently, the problem is not your defer placement, it is the panics.