Garbage Collection in Go
Go gives you automatic memory management without taking away pointers: you write &Point{}, get a plain address, and forget about free. But behind that convenience runs a concurrent garbage collector working right alongside your code — on the same cores, in the same milliseconds. "Not thinking about memory" in Go means "not thinking while things are fine". When things go wrong — RSS climbs, p99 jumps, the process is OOM-killed — debugging the collector is your job.
The defining difference between Go's GC and Java's or .NET's: it is non-generational and non-compacting. Objects never move, so their addresses are stable and taking a pointer to a struct field is safe — but that is exactly why there is no "cheap minor young-generation collection". The collector is tricolor, mark-and-sweep, and runs almost its entire cycle concurrently with the program: only two short stop-the-world pauses per cycle, each typically well under a millisecond. The traps appear where intuition from other runtimes fails: GOGC is not a time interval but a heap-growth percentage; runtime.GC() blocks rather than launching a background collection; GOMEMLIMIT is a soft target, not a hard limit that panics. This topic dissects the collector layer by layer — from cycle structure to fine tuning.
Topic Map
- What kind of collector Go has — concurrent tricolor mark-and-sweep, no generations, no compaction, objects never move.
- GC cycle trigger — a cycle starts on heap growth of
GOGCpercent, onGOMEMLIMIT, onruntime.GC(), or every two minutes. - The GC pacer — how the pacer decides when to start a cycle and keeps allocation from outrunning marking via mark assist.
- Stop-the-world pauses — two short STW pauses per cycle, at mark-start and mark-termination, each typically sub-millisecond.
- Concurrent marking — background mark workers scan the heap while the program runs; mark assist keeps allocation in check.
- Tricolor marking — white, grey, black, and the invariant that no black object points to a white one.
- The write barrier — the hybrid Dijkstra-Yuasa barrier intercepts pointer writes and preserves the invariant under concurrent marking.
- GC tuning —
GOGCfor the latency-versus-throughput balance,GOMEMLIMITinstead of the obsoleteballasttrick.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Calling Go's GC generational | Wrong model; Go has no young/old generations |
| Believing the collector compacts the heap and moves objects | False conclusion that Go pointers are unstable |
| Confusing mark-and-sweep with reference counting | Cannot explain cyclic references or the background cycle |
Treating GOGC as a time interval | GOGC is a heap-growth percentage, not milliseconds |
| Thinking GC runs on a fixed timer | Collection is allocation-driven, not clock-driven |
Assuming runtime.GC() is asynchronous | It blocks the calling goroutine until the cycle ends |
| Thinking STW lasts the entire marking phase | In reality two short pauses at cycle boundaries |
| Believing pause length scales with heap size | In modern Go STW barely depends on live heap size |
| Confusing the write barrier with a read barrier | Go uses a write barrier, not a read-time hook |
| Treating mark assist as a separate STW pause | It is inline work the allocating goroutine itself does |
Treating GOMEMLIMIT as a hard limit that panics | It is a soft pacing target; over it GC just runs harder |
Recommending ballast on modern Go | GOMEMLIMIT solves the same problem more honestly |
Interview Relevance
Garbage collection is a mandatory topic at any senior-level Go interview. The interviewer checks not knowledge of the phrase "mark-and-sweep" but a working model: what the collector does under the hood and how to control its behavior.
What interviewers usually check:
- What kind of collector Go has and its defining properties — concurrent, tricolor, non-generational, non-compacting.
- What triggers a GC cycle — heap growth of
GOGCpercent,GOMEMLIMIT,runtime.GC(), a forced cycle every two minutes. - What the pacer decides — cycle start timing and mark-assist debt, not the collector's thread count.
- When STW pauses occur — two short ones, at mark-start and mark-termination, and why they barely depend on heap size.
- Why concurrent marking needs a write barrier and which invariant it preserves.
- How
GOGCandGOMEMLIMITaffect the balance of latency, throughput, and memory.
A typical wrong answer: "GOGC is how much percent of RAM you may use, and runtime.GC() launches a background collection." That opens a discussion of how GOGC is a percentage of heap growth relative to live data, and runtime.GC() synchronously blocks the calling goroutine until a full cycle completes.