{} The Go Reference

Toolchain · Internals · Reference

Go Internals Cheat-Sheet

One-page map of Go internals — memory, GC, value representation, the scheduler, and the toolchain, plus the runtime and GODEBUG knobs worth memorizing.

Toolchain Reference ⏱ 4 min read Complete

The whole track on one page — the mental model, the costs, and the knobs. Each row links to the full page; skim to find what you need, then dive in.

🎯 How to read this

Internals matter for diagnosis, not daily coding: you write Go without thinking about any of this until a memory, latency, or allocation problem sends you looking. These tables are the map from a symptom to the mechanism and the knob.

The big picture

graph TD
SRC["your Go source"] -->|"compile + link"| BIN["static binary<br/>(code + runtime)"]
BIN --> RT["runtime"]
RT --> SCHED["scheduler<br/>G-M-P, sysmon"]
RT --> MEM["memory<br/>stack/heap, allocator"]
RT --> GC["garbage collector<br/>tricolor mark-sweep"]
SCHED -.-> CPU["CPUs"]
MEM -.-> GC

Memory & GC at a glance

ConceptKey factPage
StackPer-goroutine, ~2 KB start, grows/copies; free on returnstack & heap
HeapProcess-wide; GC-managed; for escaping valuesstack & heap
Escape analysisCompiler decides stack vs heap; see -gcflags=-mescape analysis
AllocatorTiered: mcache (per-P, lock-free) → mcentral → mheapallocator
Size classes~70 fixed sizes; O(1) alloc, some internal fragmentationallocator
GCConcurrent tricolor mark-sweep; non-moving; write barriergarbage collector
GCCPUFractionThe “is GC a problem?” number; usually a few %runtime introspection

Value representation

TypeSize (64-bit)Note
bool/int81alignment 1
int32/rune/float324
int/int64/pointer/float648
string16ptr + len
[]T slice24ptr + len + cap
any / interface16(type, data) — two words
struct{}0empty; map[K]struct{} = a set

Order struct fields largest-alignment first to minimize padding. An interface is (itab/type, data) — nil only when both words are nil. See memory layout and interfaces, itab & eface.

The scheduler

TermMeaning
G / M / Pgoroutine / OS thread / logical processor (run queue)
GOMAXPROCSnumber of Ps = parallelism bound (default NumCPU)
HandoffP detached from a syscall-blocked M to keep running Gs
Work-stealingidle P steals ~half a victim P’s queue
netpollerepoll/kqueue/IOCP; parks Gs on I/O, frees the M
sysmonP-less monitor: preemption, retake, netpoll, GC timers
Async preemptionSIGURG interrupts hog goroutines (Go 1.14+)

Full detail: scheduler internals.

The knobs

KnobEffect
GOGC=100GC when heap grows ~100% (default); higher = less GC, more RAM; off disables
GOMEMLIMIT=512MiBSoft total-memory ceiling (Go 1.19+); use in containers
GOMAXPROCS=NNumber of Ps; set to CPU limit in containers
GODEBUG=gctrace=1One line per GC cycle
GODEBUG=schedtrace=1000Scheduler state every 1000 ms
-gcflags=-mPrint escape / inline decisions
-gcflags=-SPrint generated assembly
-ldflags="-s -w"Strip symbol table + DWARF (smaller binary)
-ldflags="-X pkg.v=1.2.3"Stamp a value at link time
CGO_ENABLED=0Pure-Go, fully static binary
GOOS / GOARCHCross-compile target

Toolchain

ToolDoes
go tool compile -SSource → assembly
go tool linkObjects + runtime → binary
go tool nmList symbols
go version -m ./binModule + build settings baked in
go tool pprofWhere CPU/memory goes
go tool traceWhen — scheduler/GC timeline

✅ Symptom → mechanism → fix

High latency spikes → GC pauses or scheduler starvation → check gctrace/schedtrace; reduce allocation, set GOMEMLIMIT.
Memory keeps growing → a reachable leak (goroutine that never exits, unpruned map) → NumGoroutine, heap profile — the GC can’t free reachable objects.
Surprising allocations → escapes → -gcflags=-m, then cut interface boxing / return values not pointers.
Big binary → embedded runtime + symbols → -ldflags="-s -w", CGO_ENABLED=0.
Won’t run on scratch image → cgo dynamic-linked libc → CGO_ENABLED=0.

⚠️ The recurring traps

Don’t call runtime.GC() to fix GC pressure — reduce garbage instead. Don’t set GOGC=off in production — use GOMEMLIMIT. Typed-nil interfaces silently break err != nil — return literal nil. uintptr is not a reference — never hold one across statements; the GC may move/free the target. Layout/assembly are arch- and version-specific — never hard-code offsets or codegen assumptions. Optimize after measuring — pprof and benchmarks first, internals second.

See also

That’s the internals track. Next, see how these foundations power real systems across the networking & web and concurrency tracks.

Check your understanding

Score: 0 / 5

1. You see high GCCPUFraction and frequent GC cycles. What's the first fix to try?

GC cost is driven by how much garbage you create. The lever is the allocation rate: fewer escapes, pooled buffers, preallocated slices, value types. Forcing GC makes it worse; GOGC=off risks OOM. Tune the garbage, not the collector.

2. A struct{ a bool; b int64; c bool } wastes memory. Why, and what fixes it?

The int64 must be 8-aligned, forcing padding around the bools. Ordering fields from largest alignment to smallest packs them and drops the size from 24 to 16. Matters at scale (large slices); use fieldalignment to find offenders.

3. Which environment variable caps total memory to avoid OOM in a container?

GOGC is relative to live heap and can overshoot a hard container limit on a burst. GOMEMLIMIT sets a soft total-memory target the GC works to stay under, the standard defense against Kubernetes OOM-kills.

4. An interface holding a nil *T compares != nil. Why?

An interface is nil only when both its type and data words are nil. Assigning a typed nil pointer sets the type word, so the interface is non-nil — the classic typed-nil error bug. Return a literal nil on the success path.

5. Which command produces a small, fully static binary for a scratch container?

CGO_ENABLED=0 forces pure-Go (static, no libc) and -ldflags="-s -w" strips the symbol table and DWARF to shrink it. The result runs on scratch/distroless with no dependencies.

Comments

Sign in with GitHub to join the discussion.