Go¶
What experienced Go engineers forget before an interview, grouped by subtopic.
Timeline¶
Language and runtime changes¶
Library and tooling changes¶
Concurrency¶
Scheduler (GMP)¶
- Goroutines (G) run on OS threads (M) through logical processors (P);
GOMAXPROCS= number of Ps. - Default = min(logical CPUs, affinity mask); since Go 1.25 also capped by the cgroup CPU limit (rounded up, floor 2) and updated when it changes — before that, containers needed
automaxprocs. - Blocking syscall → the M is parked and the P moves to another M; network I/O goes through the netpoller and doesn't hold a thread.
- Each P has a local run queue (256 goroutines) plus a
runnextslot; an idle P takes half of another P's queue (work stealing), and every 61st schedule checks the global queue for fairness. - Preemption is asynchronous (signal-based) since Go 1.14, so tight loops no longer starve the scheduler.
- Goroutines start with a 2 KB stack that grows and is copied as needed — this is why launching hundreds of thousands is normal.
Channel axioms¶
| Operation | nil channel | closed channel |
|---|---|---|
| send | blocks forever | panics |
| receive | blocks forever | buffered values first, then zero value, ok == false |
| close | panics | panics |
Only the sender should close a channel; for range ch ends once it's closed and drained.
select¶
- Picks randomly among ready cases (avoids starvation);
defaultmakes it non-blocking. - Setting a channel variable to
nildisables its case — used to merge channels until all are closed. time.Timer/time.Tickerchannels are unbuffered since Go 1.23, fixing stale-value races withStop/Reset, and an unreferenced timer is collectible without callingStop; both need ago 1.23+ line ingo.moduntil Go 1.27 removed theasynctimerchanopt-out.
Sync primitives¶
sync.Mutexis not reentrant and must not be copied after first use (go vet'scopylockscatches this).sync.RWMutexblocks new readers once a writer is waiting, to avoid writer starvation.sync.OnceplusOnceFunc/OnceValue/OnceValues(1.21) for once-only initialization.WaitGroup.Go(f)(1.25) starts a goroutine and handlesAdd/Donefor you.
Atomics, sync.Map, sync.Pool¶
- Typed atomics (
atomic.Int64, etc., 1.19) replace the oldatomic.AddInt64(&x, ...)style. sync.Mapis optimized only for keys written once and read many times, or disjoint key sets per goroutine — a plain map + mutex is usually faster otherwise.sync.Poolitems are moved to a victim cache at each GC and freed at the next one (since Go 1.13), so an unused item survives about two GC cycles — never use it as a cache that must hold data.
Memory model and races¶
- Happens-before is established by channel send/receive, mutex lock/unlock,
sync.Once, and atomics — not by program order across goroutines. - A data race is less undefined than in C/C++, but multiword values (interfaces, slice headers, strings) can tear and corrupt memory.
-racedetects only races that actually execute during the run.- Rule of thumb: channels to hand off ownership or coordinate, a mutex to protect shared state in place.
context.Context¶
- Carries cancellation, deadlines, and request-scoped values across API and goroutine boundaries; canceling a parent cancels all children.
WithCancelCause(1.20) lets callers retrieve why a context was canceled viacontext.Cause(ctx).AfterFuncandWithoutCancel(1.21): run a function on cancellation, or derive a context that keeps values but drops the parent's cancellation.- Pass as the first parameter, never store in a struct; use
WithValueonly for request-scoped data, not optional parameters. - Every
WithCancel/WithTimeout/WithDeadlinereturns acancelfunc: call it (defer cancel()) or the child context and its timer live until the parent ends;go vetflags it (lostcancel).
Patterns and goroutine leaks¶
- Worker pool, fan-in/fan-out, pipeline, and semaphore (buffered channel
make(chan struct{}, n)) are the standard shapes. errgroup.Group(golang.org/x/sync/errgroup) runs goroutines, returns the first error, and can cap concurrency withSetLimit.- A goroutine leak is a goroutine blocked forever — a send nobody receives, a receive on a channel that never closes, or a loop that ignores
ctx.Done(). Detect with the pprof goroutine profile orgo.uber.org/goleak. testing/synctest(1.25) runs concurrent code in a fake-clock "bubble" so time-dependent tests run instantly and deterministically.
Memory management¶
Escape analysis¶
- The compiler decides stack vs heap by proving whether a value outlives the function (escape analysis); see the decisions with
go build -gcflags=-m. - Common causes: returning a pointer, storing a pointer in a heap object/global, boxing a value into an interface, closures capturing variables, and slices whose size isn't known at compile time.
Garbage collector¶
- Concurrent, tri-color mark-sweep; non-generational and non-compacting; a write barrier keeps marking correct while the program runs.
GOGC(default 100) controls heap growth before the next cycle;GOMEMLIMIT(1.19) sets a soft memory cap, useful in containers.- Green Tea GC improves memory locality by scanning objects in larger, contiguous spans; introduced experimental (
GOEXPERIMENT=greenteagc) in 1.25, it became the default collector in 1.26.
Reducing allocations¶
- Preallocate slices and maps when the size is known (
make([]T, 0, n)). - Reuse buffers via
sync.Pool; build strings withstrings.Builder. - Avoid needless
[]byte↔stringconversions — both copy, except where the compiler proves it safe (map lookupsm[string(b)]).
Struct layout and padding¶
- Fields are aligned to their type's alignment, so on 64-bit
struct{ a bool; b int64; c bool }takes 24 bytes while{b int64; a, c bool}takes 16 — order fields largest first in hot, numerous structs. - An empty struct (
struct{}) is zero bytes — used for sets (map[K]struct{}) and signal channels.
Slices, maps, strings¶
Slice mechanics¶
- A slice header is
{pointer, len, cap}; copying the header still shares the backing array. appendgrows in place whilelen < cap; otherwise it allocates roughly 2× under 256 elements, then a smoother ~1.25× for larger slices, and copies.- Aliasing gotcha:
b := a[:2]; append(b, x)can overwritea[2]if capacity allows. Use the full slice expressiona[lo:hi:max]to cap capacity and force a fresh allocation on append. - A small subslice of a huge array keeps the whole array alive for the GC — copy out the needed part if keeping it long-term.
Maps¶
- Hash table; implemented as Swiss tables since Go 1.24 (faster, but iteration order was already randomized and remains so).
- Concurrent write (or write + read) is a race; the runtime detects it best-effort, and then it's a fatal, unrecoverable error, not a panic you can
recover. - Maps never shrink after deletions;
clear(m)(1.21) empties one without reallocating the header. - Map values aren't addressable:
&m[k]andm[k].field = xdon't compile for struct values. - Reading a nil map returns zero values, but writing to a nil map panics —
makeit first (a nil slice, by contrast, works withappend).
Strings¶
- Immutable byte sequence, usually UTF-8;
len(s)counts bytes,range sdecodes and yields runes with byte offsets. - Invalid UTF-8 encountered during
rangeyieldsU+FFFD. string↔[]byteconversions copy; substrings share the original's backing memory.
Types and interfaces¶
Interface internals¶
- An interface value is a (type, value) pair; it is
nilonly when both are nil — a nil*Tstored in anerroris not== nil. - Comparing two interface values panics if both hold the same non-comparable dynamic type (e.g.
[]int); different dynamic types just compare unequal. - Compile-time satisfaction check:
var _ io.Reader = (*MyReader)(nil).
Method sets and embedding¶
- Pointer-receiver methods are only in the method set of
*T, so only*Tsatisfies an interface requiring them. - Embedding promotes fields/methods but is not inheritance: no polymorphism — a promoted method calling its own method calls its own version, never an outer "override." A same-named outer method shadows it.
- An embedded interface in a struct that's left unimplemented panics only when the missing method is actually called.
Generics¶
- Constraints are interfaces defining a type set;
~intaccepts any type whose underlying type isint;comparableallows==/map keys. - Compiled via GC-shape stenciling — types with the same underlying shape share code, which can still be slower than hand-specialized code in hot paths.
- Generic methods since Go 1.27: a method may declare its own type parameters, but interface methods can't, and a generic method can't satisfy an interface method. Still no specialization.
Iterators and loop variables¶
- Range-over-func iterators (
iter.Seq,iter.Seq2, 1.23) letrangework over a function, powering theslices/mapsiterator helpers. - Since Go 1.22, each iteration gets its own copy of variables declared by the loop (
:=) — the classic closure-capture bug needsgo 1.21semantics or a reused outer variable (for i = 0; ...).
Errors, defer, panic¶
Error wrapping¶
fmt.Errorf("...: %w", err)wraps;errors.Iswalks the chain comparing to a sentinel,errors.Asextracts a concrete type. Never compare a wrapped error with==.errors.Join(1.20) combines multiple errors into one thatIs/Ascan still unwrap.
defer and panic¶
deferarguments are evaluated immediately, execution is LIFO; a deferred closure can modify named return values.deferinside a long-running loop accumulates — calls only run at function return, not at loop end.recover()only stops a panic when called directly inside a deferred function; an unrecovered panic in any goroutine kills the whole process.
Standard library gotchas¶
net/http clients and servers¶
- The default
http.Clienthas no timeout — a hung server blocks the goroutine forever; always setTimeoutor use a context. - Always close
resp.Body(and drain it) or the connection isn't reused and leaks. http.ServerneedsReadHeaderTimeout/ReadTimeout/WriteTimeout, or slow clients (Slowloris) hold connections open.ServeMuxpatterns since 1.22 take methods and wildcards:mux.HandleFunc("GET /items/{id}", h)withr.PathValue("id").
encoding/json gotchas¶
- Numbers decoded into
anybecome float64, so integer IDs above 2⁵³ lose precision — useDecoder.UseNumberor a typed field. - A nil slice or map encodes as
null, an empty one as[]or{}. - Unexported fields are silently skipped; unknown JSON fields are ignored unless
DisallowUnknownFields; field-name matching is case-insensitive. omitemptynever omits a struct value (a zerotime.Timeincluded);omitzero(1.24) does.
Modules and tooling¶
Minimal version selection¶
- Go picks the minimum version satisfying every
require(MVS), not the latest — builds are reproducible without a lock file. go.sumholds checksums verified against the public checksum database; it isn't a lock file.
Module layout and workspaces¶
- Major versions ≥ 2 change the import path (
example.com/lib/v2). internal/packages are importable only from within the parent tree.go.work(1.18) builds several local modules together withoutreplacedirectives.
Compatibility and GODEBUG¶
- Potentially breaking changes ship behind
GODEBUGsettings whose defaults follow the main module'sgoline, so a toolchain upgrade alone keeps their old behavior (other runtime and performance changes still apply). - Override per program with
//go:debugdirectives or theGODEBUGenvironment variable.
Testing and profiling¶
Test tooling¶
- Benchmarks use
b.Loop()(1.24), which replaced the manualfor i := 0; i < b.N; i++idiom and avoids some compiler over-optimization pitfalls. - Fuzzing (
func FuzzX, 1.18) generates inputs from a seed corpus.
Profiling¶
- pprof profiles: CPU, heap (alloc vs inuse), goroutine, block, mutex — collected via test flags or
net/http/pprofin a running service. go tool traceshows scheduler and latency behavior over time, complementing pprof's aggregate view.- Go 1.27 adds a goroutine leak profile that reports goroutines blocked on unreachable channels or locks.
Profile-guided optimization¶
- PGO (GA in 1.21): commit a production CPU profile as
default.pgoin the main package, and builds use it automatically. - The compiler inlines hot calls and devirtualizes hot interface calls — typically 2–14% less CPU.