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.
Functions and Packages
Functions are the building blocks of Go programs. Go functions have several features that set them apart from other languages: multiple return values (used everywhere for error handling), first-class function values (functions can be passed around like any other value), and closures (functions that capture variables from their surrounding scope).Function Syntax
Multiple Return Values
Go functions can return multiple results. This is one of Go’s most distinctive features and is foundational to the entire error-handling philosophy: virtually every function that can fail returns(result, error).
(result, error) convention is so pervasive that it shapes how you read Go code. When you see two return values, your brain should immediately pattern-match to “value and error”:
Named Return Values
Return values can be named. If so, they are treated as variables defined at the top of the function. A “naked” return statement returns the named return values.Variadic Functions
Functions can take a variable number of arguments.Packages
Every Go program is made up of packages. Programs start running in packagemain.
Imports
Exported Names
In Go, a name is exported if it begins with a capital letter. For example,Pizza is an exported name, as is Pi, which is exported from the math package.
pizza and pi do not start with a capital letter, so they are not exported.
Package Initialization (init)
Variables can be initialized during declaration, but init functions allow for more complex initialization logic.
init functions run automatically before main.
First-Class Functions and Closures
Functions in Go are first-class values — you can assign them to variables, pass them as arguments, and return them from other functions. This enables powerful patterns like callbacks, middleware, and functional options.Interview Deep-Dive
Go functions can return multiple values. How does this shape Go's error handling philosophy, and why did the designers choose this over exceptions?
Go functions can return multiple values. How does this shape Go's error handling philosophy, and why did the designers choose this over exceptions?
Strong Answer:
- Multiple return values are the foundation of Go’s “errors as values” philosophy. The convention is
(result, error)— virtually every function that can fail returns its result and an error. The caller must explicitly handle the error at the call site withif err != nil. - The designers chose this over exceptions for several reasons. First, exceptions create invisible control flow: a function call in Java might throw an exception that unwinds the stack across multiple frames to a catch block you forgot existed. In Go, every error is handled right where it occurs, making the control flow explicit and local. Second, exceptions encourage lazy error handling — developers wrap large blocks in try/catch and handle everything generically, losing context. Go’s pattern forces you to think about each error individually.
- The trade-off is verbosity. Go code has significantly more
if err != nilblocks than equivalent code in Python or Java. This is the single most common complaint about Go, and the Go team has acknowledged it. But the benefit is that when you read Go code, you always know exactly where errors are handled and what happens when things fail. - In practice, the pattern becomes muscle memory: call function, check error, wrap with context using
fmt.Errorf("doing X: %w", err), return. Each layer adds context, creating an “error trace” like"creating order: charging payment: connecting to stripe: dial tcp: timeout".
_ in the context of multiple return values, and when is it acceptable to use it?The blank identifier _ discards a return value, telling the compiler you intentionally do not need it. The only time it is acceptable to discard an error is when the function genuinely cannot fail in your context (like fmt.Println writing to stdout) or when the error is truly irrelevant to program correctness. In practice, I almost never discard errors — even in logging code, a write failure to stdout can indicate a broken pipe that you should know about. Linters like errcheck will flag discarded errors, and most production codebases require them to pass clean. The one place where _ is routinely used with error returns is in test code where you have already validated the happy path and are testing a specific branch.Explain closures in Go. What does it mean for a function to 'close over' a variable, and what is the classic goroutine-closure bug?
Explain closures in Go. What does it mean for a function to 'close over' a variable, and what is the classic goroutine-closure bug?
Strong Answer:
- A closure is a function value that references variables from outside its body. The function “closes over” those variables, meaning it captures a reference to them, not a copy of their values. The captured variables survive as long as the closure exists, even after the enclosing function returns. This is how Go implements stateful function values — the classic example is a counter function that returns an incrementing closure.
- Under the hood, when the compiler detects that a local variable is captured by a closure, it moves that variable from the stack to the heap (escape analysis). The closure struct holds a pointer to the heap-allocated variable. This is why closures are slightly more expensive than regular functions — they involve heap allocation and indirection.
- The classic goroutine-closure bug (pre-Go 1.22): launching goroutines in a loop where the closure captures the loop variable. All goroutines share the same variable, and by the time they execute, the loop has finished, so they all see the final value. The fix is
i := ishadowing to create a per-iteration copy, or passing the value as a function argument. - Go 1.22 changed loop variable semantics so each iteration gets its own variable, which fixes the most common manifestation of this bug. But the underlying principle still applies: if you capture a variable by reference in a closure that runs asynchronously, you must understand that the variable may change before the closure executes.
Go uses init() functions for package initialization. Walk me through the execution order of init functions across multiple packages, and explain why overusing init() is considered an anti-pattern.
Go uses init() functions for package initialization. Walk me through the execution order of init functions across multiple packages, and explain why overusing init() is considered an anti-pattern.
Strong Answer:
- Init functions run automatically after all variable declarations in a package are evaluated. The execution order across packages follows the import dependency graph: if package A imports package B, all of B’s init functions run before A’s. Within a single package, init functions run in the order they appear in source files (sorted by filename), and a single file can have multiple init functions.
- Init functions cannot return errors, cannot be called explicitly, and run as a side effect of importing a package. This last point is the core of why overusing them is an anti-pattern: just importing a package triggers its init functions, which means your program has invisible side effects at import time. This makes testing difficult because you cannot control when initialization happens, and it makes debugging painful because the execution flow is implicit.
- The legitimate use cases are narrow: registering database drivers (
import _ "github.com/lib/pq"), registering codecs or serialization formats, and setting up package-level constants that require computation. These are truly package-level setup that happens once and cannot fail. - The anti-pattern is using init for anything that can fail (database connections, HTTP clients, configuration loading) or anything that has dependencies on external state. If your init function panics, your entire program crashes before main even starts, with no opportunity for graceful error handling. The idiomatic alternative is explicit initialization in main: create your dependencies, check for errors, and pass them down through your call chain via constructor injection.
import _ "pkg") and why would you use one?A blank import imports a package solely for its side effects — specifically, its init functions. The blank identifier _ tells the compiler you are not using any exported names from the package, which would normally cause a compile error. The most common use is database drivers: import _ "github.com/lib/pq" triggers pq’s init function, which calls sql.Register("postgres", &Driver{}) to register itself with the database/sql package. After that, you can use sql.Open("postgres", connStr) without directly referencing the pq package. This is an elegant pattern for plugin-style architectures, but it should be used sparingly because it creates hidden dependencies — someone reading your code has to know that the blank import is essential, even though it looks like it does nothing.