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.
If with Short Statement
You can execute a short statement before the condition. Variables declared here are scoped to theif block. This is one of Go’s most idiomatic patterns — you will see it everywhere in production code, especially for error handling:
err from an earlier call.
For Loops
Go has only one looping construct: thefor loop. It can be used in three ways.
1. Standard C-style Loop
2. While-style Loop
If you omit the init and post statements, it acts like awhile loop.
3. Infinite Loop
If you omit the condition, it loops forever.Switch Statements
Go’sswitch is more powerful than in C.
- No
breakneeded (it’s implicit). - Cases don’t need to be constants or integers.
Switch with no condition
This is a clean way to write long if-then-else chains.Defer
Adefer 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.
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.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).
Interview Deep-Dive
Explain defer's execution semantics in detail. When are arguments evaluated, when does the deferred function run, and what is the execution order with multiple defers?
Explain defer's execution semantics in detail. When are arguments evaluated, when does the deferred function run, and what is the execution order with multiple defers?
- Defer has three key rules. First, arguments to the deferred function are evaluated immediately at the point where
deferis called, not when the deferred function actually executes. Sodefer fmt.Println(x)captures the current value ofx. 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()aftermu.Lock()is safe even if the critical section panics. This is the mechanism that makesrecover()work — it can only be called from a deferred function because deferred functions are the only code that runs during stack unwinding.
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.Go only has one loop construct: `for`. Why was this a deliberate design decision, and how do you express patterns that other languages handle with `while`, `do-while`, or `foreach`?
Go only has one loop construct: `for`. Why was this a deliberate design decision, and how do you express patterns that other languages handle with `while`, `do-while`, or `foreach`?
- 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
forin three forms: the C-stylefor init; condition; post, the while-stylefor condition, and the infiniteforwith explicit break. Therangeclause 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 rangeon channels:for msg := range chreads 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.
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.In a production service, you have a function that opens a file, queries a database, and makes an HTTP call -- each needing cleanup. Walk me through how you would structure the defers and error handling.
In a production service, you have a function that opens a file, queries a database, and makes an HTTP call -- each needing cleanup. Walk me through how you would structure the defers and error handling.
- 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, becauserows.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 usehttp.MaxBytesReaderto limit the body size.
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.