Skip to content

Table of Contents

Core Go Language Questions

40+ questions on the Go language itself. Difficulty: 🟢 junior · 🟡 mid · 🔴 senior.

Table of Contents


Types & Values

🟢 What are Go's zero values and why do they matter? Every type has a zero value that variables get when declared without an initializer: `0` for numerics, `false` for bool, `""` for string, `nil` for pointers, slices, maps, channels, functions and interfaces, and a struct with all fields zeroed. Go guarantees there is no "uninitialized garbage" memory. This enables the "make the zero value useful" idiom: `sync.Mutex`, `bytes.Buffer`, and `strings.Builder` are all usable at their zero value without a constructor. It also means you must distinguish "not set" from "set to zero" yourself (e.g. with a pointer or an `ok` flag).
🟢 What is the difference between a value type and a reference type in Go? Go technically has no "reference types" in the C++ sense; everything is passed by value (a copy). However, some built-in types contain internal pointers, so copying the value copies a small header that still points at shared backing data: slices, maps, channels, and function values behave reference-like. Arrays, structs, and primitives are fully copied. So copying a slice header is cheap and both copies see the same elements, but copying a `[1000]int` array copies all 1000 ints.
🟡 What is the difference between `int`, `int32`, `int64`, and `rune`/`byte`? `int` and `uint` are platform-dependent (32-bit on 32-bit platforms, 64-bit on 64-bit platforms) and are the default for indexing and counts. `int32`/`int64` are fixed-width. `byte` is an alias for `uint8` (used for raw bytes), and `rune` is an alias for `int32` (used for Unicode code points). They are distinct types, so you must convert explicitly; Go has no implicit numeric conversion, which prevents many subtle bugs but means `var x int64 = someInt` won't compile without `int64(someInt)`.
🟡 How do typed and untyped constants work? Constants in Go can be untyped, meaning they have a "default type" but adapt to context with arbitrary precision until assigned. `const x = 1 << 40` is fine even though it overflows `int32`, as long as you never assign it to a too-small type. Untyped constants allow expressions like `var f float64 = 3` (the `3` becomes a float) without an explicit conversion. Once you give a constant a type (`const x int = 3`), it loses that flexibility and behaves like a typed value. `iota` generates successive untyped integer constants within a `const` block, commonly used for enums.
🔴 What does `iota` do and what are its gotchas? `iota` is a per-`const`-block counter that starts at 0 and increments by one for each `ConstSpec` (line) in the block, regardless of whether `iota` is used on that line. You can build flags with `1 << iota`, skip values with `_`, and define enums. A common gotcha: `iota` resets to 0 in each new `const (...)` block, and it increments per line, not per identifier, so multiple constants on one line share the same `iota` value.
const (
    _  = iota             // 0, skipped
    KB = 1 << (10 * iota) // 1 << 10
    MB                     // 1 << 20
    GB                     // 1 << 30
)

Slices & Arrays

🟢 What is the difference between an array and a slice? An array has a fixed length that is part of its type (`[3]int` and `[4]int` are different types) and is a value: assigning or passing it copies all elements. A slice is a descriptor — a three-word header of `{pointer, length, capacity}` — that points into an underlying array. Slices are dynamically sized views and are what you use 99% of the time; arrays are mostly used as the backing store or for fixed-size data like hashes.
🟡 What are the three components of a slice header? A slice header is `struct { ptr *T; len int; cap int }`. `ptr` points to the first element of the slice within some backing array, `len` is how many elements are accessible via indexing (`s[0]`..`s[len-1]`), and `cap` is how many elements exist from `ptr` to the end of the backing array. `append` can grow up to `cap` in place; beyond that it allocates a new, larger backing array and copies. You can reslice within capacity with the full slice expression `s[low:high:max]` to also cap the resulting slice.
🔴 Explain this aliasing bug: appending to a slice modifies another slice. Slicing does not copy the backing array, so two slices can share storage. If a sub-slice has spare capacity, `append` writes into the shared array and silently mutates the other slice.
a := []int{1, 2, 3, 4, 5}
b := a[:2]            // len 2, cap 5 — shares a's array
b = append(b, 99)    // writes into a[2]!
fmt.Println(a)       // [1 2 99 4 5]  <-- surprise
Fix by forcing a copy with a three-index slice that caps capacity (`a[:2:2]`), which makes the next `append` allocate, or by using `slices.Clone`. This is the classic "append aliasing" interview trap.
🟡 How does `append` grow a slice, and what is the growth strategy? When `len == cap`, `append` allocates a new backing array, copies existing elements, and returns a new header — which is why you must always write `s = append(s, x)`. Historically Go doubled capacity for small slices and grew ~1.25x for large ones; since Go 1.18 the threshold and factor were tuned (roughly doubling under 256 elements, then growing by a smaller factor). The exact factor is an implementation detail you should not rely on. If you know the final size, preallocate with `make([]T, 0, n)` to avoid repeated reallocations and copies.
🟡 What is the difference between `make([]int, 5)` and `make([]int, 0, 5)`? `make([]int, 5)` creates a slice with length 5 and capacity 5, filled with zero values, so indices `0..4` are immediately valid. `make([]int, 0, 5)` creates a slice with length 0 and capacity 5, so it holds no elements yet but can grow to 5 via `append` without reallocating. A common bug is using the first form and then `append`-ing, which leaves five leading zeros followed by your data.
🔴 Why can passing a slice to a function sometimes mutate the caller's data and sometimes not? The slice header is passed by value, so the function gets its own `{ptr,len,cap}` copy — but `ptr` still points at the same backing array. Therefore modifying existing elements (`s[i] = x`) is visible to the caller, but `append` may or may not be: if it grows beyond capacity it allocates a new array that only the callee's copy points to, so the caller sees nothing. This is why functions that grow a slice must return it. To always mutate in place, pass a pointer to the slice or return the result.
🟡 How do you delete an element from a slice? For order-preserving deletion, shift the tail down with `append` (or use `slices.Delete` since Go 1.21):
s = append(s[:i], s[i+1:]...) // remove index i, preserves order, O(n)
If order does not matter, swap the last element into the gap and truncate, which is O(1): `s[i] = s[len(s)-1]; s = s[:len(s)-1]`. Note that order-preserving deletion leaves the old last element referenced by the backing array; if elements are pointers, zero them out to avoid memory leaks (`slices.Delete` handles this).
🔴 Why might a small slice keep a huge array alive, and how do you fix it? Slicing keeps the entire backing array reachable as long as the slice exists, because the header's pointer references it. If you read a 100MB file into a byte slice and keep a 10-byte sub-slice, all 100MB stays live and uncollectable. To release the rest, copy the bytes you need into a fresh slice (`out := slices.Clone(small)` or `append([]byte(nil), small...)`), which lets the GC reclaim the large array.

Maps

🟢 How do you check whether a key exists in a map? Use the comma-ok form, because indexing a missing key returns the value type's zero value, which is indistinguishable from a real zero-valued entry.
v, ok := m["key"]
if !ok { /* key absent */ }
Reading a missing key never panics; it just returns the zero value. Only writing to a `nil` map panics.
🟡 Are Go maps ordered? How do you iterate in a deterministic order? No. Map iteration order is intentionally randomized: the runtime starts each `range` over a map at a random bucket to prevent code from depending on order. To iterate deterministically, collect the keys into a slice, sort them, then iterate the sorted slice. This randomization was a deliberate language decision to surface ordering bugs early.
🔴 Are maps safe for concurrent use? What happens if they aren't? Plain maps are not safe for concurrent access when at least one goroutine writes. The runtime has a built-in race detector for maps: a concurrent write triggers a fatal `"concurrent map writes"` panic that you cannot `recover` from, because it indicates memory corruption. Protect maps with a `sync.RWMutex`, or use `sync.Map` for specific patterns (write-once/read-many or disjoint key sets). `sync.Map` is not a general replacement and is often slower for read-write-heavy workloads with overlapping keys.
🟡 Why can't you take the address of a map element (`&m[k]`)? Map values are not addressable because the map may rehash and move its entries to a new bucket array as it grows, which would invalidate any pointer into it. So `&m[key]` is a compile error. If you need to mutate a struct stored in a map, either store a pointer (`map[K]*V`) and mutate through it, or read-modify-write the whole value (`v := m[k]; v.X++; m[k] = v`).
🟡 What types can be used as map keys? Any comparable type: booleans, numerics, strings, pointers, channels, interfaces, and structs/arrays composed only of comparable types. Slices, maps, and functions are not comparable and cannot be keys (the compiler rejects them). Using an interface key is allowed but risky: if the dynamic type stored in the interface is not comparable (e.g. a slice), comparing it at runtime panics. Floating-point keys are legal but discouraged due to `NaN != NaN` and precision issues.
🔴 What is the cost of pre-sizing a map with `make(map[K]V, n)`? Providing a size hint pre-allocates enough buckets to hold roughly `n` elements without rehashing, avoiding repeated incremental growth and rehashing as you insert. It is a hint, not a hard capacity, and does not change `len`. For large maps built in a tight loop this is a meaningful performance win and reduces allocation churn; for small maps it rarely matters.

Strings & Runes

🟢 What is a string in Go under the hood? A string is an immutable two-word header `{ptr, len}` pointing at a read-only sequence of bytes (conventionally UTF-8). Immutability means you cannot assign `s[i] = c`; you must convert to `[]byte` or `[]rune`, modify, and convert back. Indexing `s[i]` yields a `byte` (the i-th byte), not a character, which matters for multi-byte runes.
🟡 What is the difference between `len(s)`, ranging over a string, and `utf8.RuneCountInString`? `len(s)` returns the number of bytes, not characters. Ranging with `for i, r := range s` decodes UTF-8 and yields rune values `r` with `i` being the byte offset of each rune's start. `utf8.RuneCountInString(s)` returns the number of Unicode code points. For the string "héllo", `len` is 6 (é is two bytes in UTF-8) while the rune count is 5. To reverse or index "characters," convert to `[]rune` first.
🟡 Why is repeated string concatenation in a loop a bad idea, and what should you use? Strings are immutable, so `s += x` allocates a brand-new string and copies both operands every iteration, giving O(n²) behavior and heavy GC pressure. Use `strings.Builder`, which writes into a growable internal byte buffer and produces the final string once with no extra copy:
var b strings.Builder
for _, p := range parts {
    b.WriteString(p)
}
result := b.String()
For known sizes, call `b.Grow(n)` first. `bytes.Buffer` works too but `strings.Builder` avoids a copy in `String()`.
🔴 What does converting between `string` and `[]byte` cost, and how does the compiler optimize it? `[]byte(s)` and `string(b)` generally allocate and copy, because strings are immutable and slices are mutable — sharing memory would break that guarantee. However, the compiler optimizes common patterns to avoid the copy: `for i, c := range []byte(s)`, map lookups like `m[string(b)]`, and string comparisons/concatenation with `string(b)` operands can avoid allocation because the runtime can prove the temporary is not mutated. In hot paths, prefer working with `[]byte` end-to-end (e.g. `bytes` package mirrors `strings`).

Pointers & Memory

🟢 Does Go have pointers? Does it have pointer arithmetic? Yes, Go has pointers (`*T`, `&x`, `*p`) but no pointer arithmetic in safe code — you cannot do `p++` to walk memory. This keeps the language memory-safe and lets the garbage collector and compiler reason about what is reachable. Raw pointer manipulation is only possible via the `unsafe` package, which bypasses type and memory safety and should be avoided unless absolutely necessary.
🟢 When should you use a pointer versus a value? Use a pointer when you need to mutate the receiver/argument, when the struct is large and copying is expensive, or when the type's identity matters (e.g. a `sync.Mutex` must not be copied). Use a value when the data is small, immutable in spirit, or you want copy semantics for safety. Be consistent within a type's method set: if any method needs a pointer receiver, usually make them all pointer receivers.
🟡 Is it safe to return a pointer to a local variable in Go? Yes — unlike C, this is perfectly safe. Go's escape analysis detects that the local variable's address outlives the function and allocates it on the heap instead of the stack. The garbage collector keeps it alive as long as the pointer is reachable.
func newCounter() *int {
    c := 0      // escapes to heap
    return &c   // safe
}
🟡 What is the difference between `new(T)` and `&T{}`? Both allocate and return a `*T`. `new(T)` allocates a zeroed `T` and returns its address — useful for any type including non-composite ones like `new(int)`. `&T{...}` is a composite literal that lets you initialize fields at the same time and is far more common for structs. `new(T)` is equivalent to `&T{}` only when you want all-zero fields. There is no semantic difference in where memory comes from; escape analysis decides stack vs heap for both.
🔴 What is the zero value of a pointer, and what happens when you dereference it? The zero value of a pointer is `nil`. Dereferencing a `nil` pointer panics with a runtime `"invalid memory address or nil pointer dereference"` (SIGSEGV) error. A subtle case: calling a method with a pointer receiver on a `nil` pointer does not panic by itself — it only panics if the method body dereferences the receiver. Some types deliberately support `nil` receivers (e.g. a linked-list node's `Len()` returning 0 for a nil node).

Structs & Embedding

🟢 What is struct embedding and how does it differ from inheritance? Embedding places a type inside a struct without a field name, promoting the embedded type's fields and methods to the outer struct. It is composition, not inheritance: there is no subtype polymorphism and no virtual dispatch through the parent. The outer type can use promoted methods directly and can override them by defining a method with the same name. The embedded value is just a regular field accessible by its type name (`outer.Inner`).
type Logger struct{ prefix string }
func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }

type Service struct {
    Logger // embedded
    name   string
}
// s.Log("hi") works via promotion
🟡 How does Go resolve a method that exists at multiple embedding depths? Method and field promotion follows the shallowest-depth-wins rule. A method or field declared directly on the outer struct shadows any promoted one. If two types are embedded at the same depth and both provide a method with the same name, the reference is ambiguous and the program fails to compile unless you qualify it explicitly (`s.Inner.Method()`). This shallowest-wins rule is how you "override" embedded behavior.
🔴 What does it mean to embed an interface in a struct or another interface? Embedding an interface in a struct gives the struct that interface's method set by delegating to whatever concrete value is stored in the embedded field; if the field is `nil`, calling those methods panics. This is a common pattern to satisfy a large interface while only overriding a few methods (e.g. wrapping `http.ResponseWriter`). Embedding an interface in another interface composes their method sets (`io.ReadWriter` embeds `io.Reader` and `io.Writer`). It is a powerful way to build decorators and partial implementations.
🟡 What are struct tags and how are they used? Struct tags are string metadata attached to fields, written in backticks, and read at runtime via reflection. Libraries define their own conventions: `encoding/json` uses `json:"name,omitempty"`, database mappers use `db:"..."`, validators use `validate:"..."`. The compiler does not interpret tags; they are inert strings until a library reflects on them. Malformed tags silently do nothing, which is a common source of "why isn't my JSON field renamed" bugs.
type User struct {
    ID    int    `json:"id"`
    Email string `json:"email,omitempty"`
}
🔴 What is an empty struct `struct{}` used for? `struct{}` occupies zero bytes, so it is the idiomatic "no data" placeholder. The two main uses are a set (`map[string]struct{}`) where you only care about key presence and want zero per-value overhead, and a signal-only channel (`chan struct{}`) for done/close notifications where the value carries no information. All instances of `struct{}` share the same address, so they cost nothing to store.

Interfaces

🟢 What is an interface in Go and how is it satisfied? An interface is a type that specifies a method set. A concrete type satisfies an interface implicitly — there is no `implements` keyword — simply by having all the required methods. This structural, duck-typed approach decouples implementations from the interfaces they satisfy, so you can define an interface in the consuming package even for types you do not control. Interface satisfaction is checked at compile time when you assign a concrete value to an interface variable.
🔴 Why is a nil interface different from an interface holding a nil pointer? (The "typed nil" trap) An interface value is a pair `(type, value)`. It equals `nil` only when both halves are nil. If you store a `nil` *concrete pointer* into an interface, the type half is set, so the interface is non-nil even though the underlying pointer is nil.
func do() error {
    var p *MyError = nil
    return p          // returns a NON-nil error holding (*MyError, nil)
}
if do() != nil { /* this runs! bug */ }
This is the most common Go gotcha. Fix it by returning a literal `nil` (declare the return value as `error` and assign `nil`), and never return a concrete typed nil pointer through an interface.
🟡 What is the empty interface `interface{}` / `any`, and when should you use it? `interface{}` (aliased as `any` since Go 1.18) has no methods, so every type satisfies it — it can hold any value. Use it when you genuinely cannot know the type ahead of time: `fmt.Println`, JSON decoding into `map[string]any`, or heterogeneous containers. It comes at the cost of static type safety: you must type-assert or type-switch to do anything with the value, and you lose compile-time checks. Since generics (1.18), prefer type parameters over `any` when you want type safety with flexibility.
🟡 What is a type assertion and how do you do it safely? A type assertion `x.(T)` extracts the concrete value of type `T` from an interface. The single-return form `v := x.(T)` panics if the dynamic type isn't `T`; the comma-ok form `v, ok := x.(T)` never panics and sets `ok` to false on mismatch. Use the comma-ok form unless a wrong type is truly a programmer error. A type switch (`switch v := x.(type)`) is the idiomatic way to handle several possible types.
🔴 How is an interface value represented in memory, and what does the method call cost? A non-empty interface is two words: an `itab` pointer (which holds the dynamic type plus a method dispatch table) and a data pointer to the concrete value. The empty interface is `{type, data}`. Calling an interface method is an indirect call through the itab, so it cannot be inlined and is slightly more expensive than a direct call, and it may force the value to escape to the heap. Converting a small value to an interface can also cause a heap allocation to box it. These costs are usually negligible but matter in tight hot loops.
🟡 Should interfaces be small? What does "accept interfaces, return structs" mean? Yes — Go favors small interfaces, ideally one or two methods (`io.Reader`, `io.Writer`, `fmt.Stringer`), because small interfaces are easy to implement and compose. "Accept interfaces, return concrete types" means functions should take the narrowest interface they need (maximizing reusability and testability) but return concrete types so callers have full access to the value and you do not prematurely constrain the API. Define interfaces in the consumer package, where they are used, not the producer.

Methods & Receivers

🟡 What is the difference between a value receiver and a pointer receiver? A value receiver operates on a copy, so mutations don't affect the original and it is safe for concurrent reads; a pointer receiver can mutate the original and avoids copying large structs. Choose pointer receivers when you must mutate state, the struct is large, or the type contains a `sync.Mutex` (which must not be copied). A practical rule: be consistent — if one method needs a pointer receiver, give all methods pointer receivers so the method set is uniform.
🔴 How do receivers affect a type's method set and interface satisfaction? The method set of `T` includes only value-receiver methods; the method set of `*T` includes both value- and pointer-receiver methods. So if a method has a pointer receiver, only `*T` satisfies an interface requiring it — a value `T` does not. You can call pointer-receiver methods on an addressable value (`t.Method()` works because Go auto-takes `&t`), but you cannot do so on a non-addressable value such as a map element or the result of a function call.
m := map[int]T{}
m[0].PointerMethod() // compile error: m[0] is not addressable
🟡 Can you define methods on any type? What are the restrictions? You can define methods only on named types declared in the same package as the method. This means you cannot add methods to `int`, `[]string`, or types from other packages directly — but you can define a new named type (`type MyInt int`) and add methods to it. This restriction preserves package boundaries and prevents action-at-a-distance modifications to other packages' types. To extend an external type, embed it in a new struct or wrap it.
🔴 What is a method value versus a method expression? A method value binds the receiver: `f := t.Method` captures `t` and gives a func you call as `f(args)`. A method expression leaves the receiver as the first parameter: `f := T.Method` gives `func(T, args)` (or `(*T).Method` for pointer receivers). Method values are handy for callbacks (`go t.Run`), while method expressions are useful when you want to apply the method across many receivers. Note a method value evaluates and copies the receiver at the time it is created.

defer, panic & recover

🟢 What does `defer` do and in what order do deferred calls run? `defer` schedules a function call to run when the surrounding function returns, whether it returns normally or via a panic. Deferred calls run in LIFO order (last deferred, first executed). It is the idiomatic way to pair acquisition with release — `f.Close()`, `mu.Unlock()`, `wg.Done()` — so cleanup is co-located with setup and runs on every exit path.
🟡 When are a deferred call's arguments evaluated? A deferred call's arguments (and the function value itself) are evaluated immediately when the `defer` statement executes, not when the deferred call runs. So `defer fmt.Println(i)` captures `i`'s current value, whereas `defer func(){ fmt.Println(i) }()` reads `i` at execution time and sees its final value.
i := 0
defer fmt.Println(i)        // prints 0
defer func() { fmt.Println(i) }() // prints 1
i++
🔴 How can a deferred function modify a function's return value? Only if the function uses named return values. A deferred closure can read and assign the named results after the `return` statement sets them but before the function actually returns, which is how you wrap or translate errors and recover from panics.
func safe() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    panic("boom")
}
With anonymous (unnamed) returns there is nothing for the defer to assign to, so it cannot change the result.
🟡 What is the difference between panic and a normal error? Errors are ordinary values returned and checked explicitly; they represent expected, recoverable conditions and are the idiomatic way to handle failure in Go. `panic` unwinds the stack, running deferred functions, and is for truly exceptional, programmer-level failures (nil dereference, index out of range, impossible invariants) — not control flow. Libraries should return errors, not panic across API boundaries; reserve panic for unrecoverable bugs or, occasionally, internal use within a package that is recovered before returning.
🔴 How does `recover` work, and what are its limitations? `recover` stops a panicking goroutine's unwinding and returns the panic value, but it only works when called directly inside a deferred function. Calling `recover` outside a defer, or from a function called by the defer, returns `nil` and does nothing. Crucially, `recover` only catches panics in the same goroutine — a panic in another goroutine that isn't recovered there crashes the whole program. Some failures cannot be recovered at all, such as `"concurrent map writes"` and stack overflow, because they signal memory corruption.
🟡 Does a panic in one goroutine get caught by a recover in another? No. `recover` is goroutine-local; it can only stop a panic propagating up its own goroutine's stack. If a goroutine panics and never recovers within itself, the entire process terminates regardless of recovers elsewhere. This is why every long-lived worker goroutine in production code typically has its own top-level `defer func(){ recover() }()` guard if a panic must not take down the process.

Generics

🟢 What are generics in Go and when were they added? Generics (type parameters) were added in Go 1.18 (March 2022). They let you write functions and types parameterized over a set of types, constrained by an interface, so you can write `Map`, `Filter`, `Min`, or a generic `Stack[T]` once instead of duplicating code or using `interface{}` with runtime assertions. The syntax uses square brackets: `func Max[T constraints.Ordered](a, b T) T`. Generics give you type safety and no boxing/assertions while keeping a single implementation.
🟡 What is a type constraint, and what does `comparable` mean? A constraint is an interface that limits which types a type parameter may take, either by listing methods or by listing allowed underlying types using union syntax (`~int | ~string`). The `~` means "any type whose underlying type is this," so it includes named types. `comparable` is a built-in constraint satisfied by types usable with `==`/`!=`, which is required for map keys and equality — it lets you write generic `Contains` or a generic set. The `golang.org/x/exp/constraints` (and standard `cmp`) packages provide common constraints like `Ordered`.
type Number interface{ ~int | ~int64 | ~float64 }
func Sum[T Number](xs []T) T { var s T; for _, x := range xs { s += x }; return s }
🔴 How are Go generics implemented — monomorphization or boxing? Go uses a hybrid called "GC shape stenciling." The compiler generates one instantiation per distinct GC shape (memory layout) rather than per concrete type, and it passes a dictionary of type-specific information at runtime. So all pointer-shaped type arguments often share a single instantiation (more like boxing, with dictionary lookups), while differently shaped types get separate code (more like monomorphization). This is a compromise between code bloat and runtime cost, and it means generic code can be slightly slower than hand-specialized code in some cases.
🟡 What is type inference in Go generics, and when must you specify types explicitly? The compiler usually infers type arguments from the function arguments, so you write `Max(3, 5)` not `Max[int](3, 5)`. You must specify them explicitly when they can't be inferred — for instance when the type parameter appears only in the return type or constraints and not in any argument (`Zero[int]()`), or when instantiating a generic type. Inference covers function arguments and, since later versions, some constraint-based cases, but return-only parameters always need explicit instantiation.
🔴 When should you NOT use generics? Avoid generics when an ordinary interface expresses the abstraction better — if you only call methods on a value, accept an interface, don't parameterize. Don't reach for generics for a single concrete type or when it makes the code harder to read for no real reuse. The Go team's guidance: use generics when you have functions that work the same way over many element types (container types, general algorithms like `slices`/`maps` packages) but prefer interfaces when behavior, not data structure, is what varies.

Error Handling

🟢 What is the idiomatic way to handle errors in Go? Functions return an `error` as the last return value, and callers check it immediately with `if err != nil`. The `error` is a built-in interface with a single `Error() string` method. There are no exceptions for ordinary failure; explicit checking makes control flow visible. Wrap errors with context as they propagate up so the final message tells a story, and only handle (log/return/translate) an error once.
🟡 What do `errors.Is`, `errors.As`, and `%w` do? `fmt.Errorf("...: %w", err)` wraps an error, preserving the original in a chain. `errors.Is(err, target)` walks that chain checking for a specific sentinel error value (e.g. `errors.Is(err, sql.ErrNoRows)`), and `errors.As(err, &target)` walks it looking for an error of a specific type and assigns it so you can read its fields. Use `%w` to wrap, `Is` to compare to sentinels, and `As` to extract typed errors — never compare error strings.
🔴 What is the difference between sentinel errors, error types, and opaque errors? Trade-offs? Sentinel errors are exported values (`var ErrNotFound = errors.New(...)`) compared with `errors.Is`; they couple callers to that value and become part of your API. Custom error types implement `error` and carry structured data, extracted with `errors.As`; more flexible but also part of your API surface. Opaque errors expose only the `error` interface and maybe behavior-probing (`interface{ Temporary() bool }`), giving the most decoupling. Modern guidance: prefer wrapping with `%w` and probing with `Is`/`As`, and keep your exported sentinel/type surface deliberately small because everything callers can inspect becomes a compatibility commitment.
🟡 Why is `if err != nil` everywhere considered idiomatic rather than a code smell? Go deliberately makes failure paths explicit and local rather than hiding them in exception handlers that jump across the stack. The verbosity is a trade for readability of control flow: you can see exactly where each operation can fail and what happens. The idiom is to add context while propagating (`return fmt.Errorf("loading config: %w", err)`) so errors accumulate a useful trace. Helper patterns (early returns, small functions) reduce the noise without hiding the checks.

Memory Model, Escape Analysis & GC

🟡 What is escape analysis and why does it matter for performance? Escape analysis is a compile-time pass that decides whether a value can live on the goroutine's stack or must "escape" to the heap. Stack allocation is essentially free (bumped/popped with the frame) and creates no GC work, while heap allocation costs an allocation and adds GC pressure. A value escapes if its lifetime can outlive the function — e.g. its address is returned, stored in a heap object, captured by a closure that escapes, or sent to an interface that escapes. Run `go build -gcflags='-m'` to see escape decisions and optimize hot paths.
🔴 What are common reasons a value escapes to the heap? Returning a pointer to a local, storing a pointer in a longer-lived structure (a global, a heap slice, a channel), capturing a variable by reference in a closure that escapes, converting a value to an interface where the compiler can't prove it stays local (interfaces hold pointers), or a value too large for the stack. Also, slices/maps with sizes the compiler can't bound, and anything passed to a function the compiler can't see/inline that takes its address. Reducing escapes — by avoiding unnecessary pointers and interface boxing in hot loops — cuts allocations and GC load.
🟡 How does Go's garbage collector work at a high level? Go uses a concurrent, tri-color mark-and-sweep collector with a write barrier, designed to minimize stop-the-world pause times (typically sub-millisecond). It runs mostly concurrently with your program: it marks reachable objects starting from roots (stacks, globals), using the write barrier to track pointers mutated during marking, then sweeps unreachable memory. It is non-generational and non-compacting. The collector trades some throughput and a little CPU for very low latency, which suits server workloads.
🔴 What is GOGC / GOMEMLIMIT and how do they tune the GC? `GOGC` (default 100) sets the heap growth percentage that triggers the next collection: at 100, the GC runs when the live heap doubles since the last cycle. Raising it (e.g. `GOGC=200`) collects less often, using more memory but less CPU; lowering it does the opposite. `GOMEMLIMIT` (Go 1.19+) sets a soft memory ceiling so the runtime collects more aggressively as you approach it, which prevents OOM kills in containers without disabling GC. A common production setup is a high/off `GOGC` combined with a `GOMEMLIMIT` to balance throughput against a hard memory budget.
🟡 What is the Go memory model and what does "happens-before" mean? The Go memory model specifies when a read in one goroutine is guaranteed to observe a write in another. Within a single goroutine, execution is sequentially consistent. Across goroutines, you need a synchronization event to establish a "happens-before" relationship: channel sends/receives, mutex lock/unlock, `sync/atomic`, `sync.Once`, and `WaitGroup` all create these guarantees. Without synchronization, concurrent access to shared memory is a data race with undefined behavior — the compiler and CPU may reorder or cache, so a value written by one goroutine may never be seen by another.
🔴 What is `sync.Pool` and when should you use it? `sync.Pool` is a per-P free list of reusable objects that reduces allocation and GC pressure for short-lived, frequently allocated objects (e.g. buffers in a hot request path). You `Get()` an object (which may be freshly allocated via the `New` func or recycled) and `Put()` it back when done, after resetting it. Caveats: the pool can be emptied at any GC, so it is a cache not a guarantee; objects must be safe to reuse and reset; and it's only worthwhile under real allocation pressure — misused, it adds complexity for no gain. `fmt` and `encoding/json` use it internally.
🔴 What is the difference between the stack and heap in Go, and who decides allocation? Each goroutine has its own small, growable stack (starting around 8KB, resized by copying) for local variables with function-bounded lifetimes; allocation/deallocation is just moving the stack pointer and incurs zero GC cost. The heap holds values that outlive their function and is managed by the garbage collector. The compiler — not the programmer — decides placement via escape analysis; you influence it by how you write code (avoiding unnecessary pointers, interface conversions, and unbounded sizes), not by an explicit keyword.

Packages, Modules & Build

🟢 What does an `init` function do and when does it run? `init()` functions run automatically at program startup after all package-level variables are initialized and before `main`. A package can have multiple `init` functions (even across files), and they run in the order the files are presented to the compiler, after the package's imports have been initialized. Use them sparingly — for registering drivers (`database/sql` drivers, image formats) or validating required configuration — since hidden initialization order and side effects can make code hard to test and reason about.
🟡 What is the difference between `go mod`, `go.mod`, and `go.sum`? `go mod` is the command set for managing modules. `go.mod` declares the module path, the Go version, and direct/indirect dependency requirements with their minimum versions. `go.sum` records cryptographic checksums of the exact module content used, so builds are verifiable and tamper-evident. Go uses Minimal Version Selection: it picks the lowest version that satisfies all requirements, making builds reproducible without a separate lockfile. Commit both files.
🟡 How does Go decide what is exported from a package? Visibility is determined by the first letter of an identifier: capitalized names (`Println`, `User`, `MaxSize`) are exported and visible to importers; lowercase names are unexported and package-private. This applies to top-level declarations and to struct fields and methods. There is no `public`/`private` keyword; the casing convention is the entire access-control mechanism, which keeps the rule simple and visible at every use site.
🔴 What are build tags / build constraints used for? Build constraints control which files are included in a build for a given target. The modern form is a `//go:build` line at the top of a file (e.g. `//go:build linux && amd64`), and file-name suffixes like `foo_linux.go` or `bar_test.go` also act as constraints. They let you provide platform-specific implementations, gate experimental code, or separate integration tests (`//go:build integration`) so they only run when you opt in. The constraint must appear before the package clause, followed by a blank line.
🟢 What is the difference between `go run`, `go build`, and `go install`? `go run` compiles and immediately executes, leaving no binary behind — handy for quick scripts. `go build` compiles and writes an executable (or just checks compilation for a library) into the current directory. `go install` compiles and installs the binary into `$GOBIN` (or `$GOPATH/bin`) so it's on your `PATH` for reuse. For production you typically `go build` with explicit flags (`-ldflags "-s -w"`, `CGO_ENABLED=0`) to produce a stripped, static binary.