Part 2 · 2 chapters · ~12 min
Memory and the Runtime
Stack and heap, escape analysis, the garbage collector (concurrent mark-sweep, GOGC and GOMEMLIMIT), goroutine stacks that grow, the G-M-P scheduler with measured goroutine costs, the netpoller, preemption, and GOMAXPROCS in containers.
5
Goroutines and the scheduler
code
// measured on this machine (go1.23.4, darwin/arm64, GOMAXPROCS 12) // started 100000 goroutines in 74.3 ms (743 ns each), stack memory ~2.0 KB each // unbuffered channel round trip: 295 ns
THE G-M-P SCHEDULER
goroutines (G) run on OS threads (M) through logical processors (P)
swipe the figure sideways, or tap expand for full screen
1/5
cheap goroutines
A goroutine starts with a small growable stack (about 2 KB measured here) and costs about 743 ns to start: 100,000 took 74 ms. Compare a Node worker at 14.6 ms or a process at 1.5 ms.
743 ns and ~2 KB each (measured)hundreds of thousands are normal
6
Escape analysis and the GC
code
go build -gcflags=-m ./... # shows which values escape to the heap and why
// ./ledger.go:12:9: &Account{...} escapes to heap (returned pointer outlives the function)
// ./ledger.go:20:14: buf does not escape (stays on the stack: free to allocate)
GODEBUG=gctrace=1 ./ledger # one line per GC: heap sizes, pause times, CPU share
GOGC=100 # default: collect when heap grows 100% since last GC
GOMEMLIMIT=900MiB # soft memory limit (Go 1.19+): GC works harder before hitting itGo's collector is concurrent and non-moving, tuned for low pause times (sub-millisecond typically) rather than maximum throughput. Fewer heap allocations means less GC work: pass small structs by value, reuse buffers with sync.Pool in hot paths, and check -gcflags=-m before optimising. In containers, set GOMEMLIMIT to about 90% of the memory limit and GOMAXPROCS to the CPU quota (Go 1.25 does the latter automatically; automaxprocs before that).