Go’s Garbage Collector
A practical look at Go’s concurrent GC and why it usually stays out of your way
“The best garbage collector is the one you never think about.”
— A senior engineer who definitely spent years thinking about garbage collectors
I still remember the first time I profiled a Go service under heavy load and saw something surprising: the garbage collector wasn’t the bottleneck. Coming from languages where GC pauses can feel like traffic lights on a busy road, it almost felt suspicious. Was the profiler broken? Was the service too simple? Or did Go’s runtime really manage to clean up gigabytes of heap data while keeping latency smooth?
The truth is that Go’s GC is one of the most quietly sophisticated parts of the language. It’s a concurrent, tri-color, non-compacting collector designed around one goal: keep the world running while memory is being reclaimed. And once you understand how it works, you start writing code that cooperates with it instead of fighting it.
Understanding Go’s Garbage Collector
Go’s garbage collector often feels invisible, which is by design. It runs concurrently with your program, it avoids long pauses, and it generally keeps memory clean without developers thinking about it. But under the hood it is a carefully engineered system that balances throughput, latency, and memory efficiency. The high level model is a concurrent, tri-color, mark-and-sweep collector that cooperates tightly with the scheduler and the allocator.
At its core, the GC divides memory into objects that are reachable and objects that are not. Reachable means there is some live pointer chain connecting the object to the program’s roots. Roots include the stacks of all goroutines, global variables, and a few runtime internal structures. Anything not reachable after a scan can be reclaimed.
The Tri Color Model
Go uses a tri color abstraction to track progress during each GC cycle.
The three-color mark-sweep is a fundamental algorithm used in garbage collection to distinguish between live (black) objects and reclaimable (white) objects. The process is simplified into a marking phase using three colors to track object state: White, Gray, and Black.
The Three Colors
⚪ White. Objects that have not yet been seen by the garbage collector. They are assumed to be garbage initially. Objects that remain White at the end of the marking phase are considered garbage and will be collected.
⚫ Black. Objects that are known to be live and safe from collection. This set includes Root objects (objects directly accessible to the application) and any objects reachable from them. The collector is done processing this object’s references.
🔘 Gray. Objects that are known to be live, but the collector has not yet scanned their references to find other live objects. They are the worklist for the marking process.
The Marking Phase
The marking phase involves iterating until no gray objects remain.
Start. The collector begins by marking all objects initially reachable from the application’s root set as Gray.
Scan and Mark.
Select a Gray object from the set of Gray objects (the worklist).
Mark this selected object as Black.
Identify all objects that the newly Black object points to.
Change the color of any of these pointed-to objects that are currently White to Gray.
Repeat: Repeat Step 2 until there are no Gray objects left in the object graph.
Collection (Sweep) Phase
When the marking phase is complete.
The heap contains only Black (live, surviving) objects and White (unreachable, garbage) objects.
The garbage collector safely reclaims all White objects.
TL;DR. Objects start out white which means unvisited. As the collector encounters them they become grey which means they have been found but their children have not been scanned. Once the GC scans all pointers inside the object it becomes black which means fully processed. By the end of the cycle only white objects with no incoming references remain, and the runtime safely reclaims them. This model also enables Go’s write barrier. Because the GC is concurrent, your program continues mutating pointers while the collector is marking. The write barrier ensures that if your code stores a pointer to a white object into a black object, the runtime immediately colors the white object grey. This guarantees correctness while work continues in parallel.
Concurrent Marking
Marking runs while your program is running. The runtime pauses the world briefly to collect root pointers then lets all goroutines resume. Mark workers run in the background scanning stacks, spans, and heap objects. Because marking competes for CPU time, Go’s scheduler cooperates by throttling goroutines slightly when GC is behind. The goal is to maintain a target heap size relative to live data which is why GOGC controls how aggressively the collector grows the heap.
GOGCis an environment variable in Go that controls the aggressiveness of the garbage collector (GC). It sets the percentage of new memory that can be allocated before the GC is triggered, which allows developers to tune the trade-off between CPU usage and memory usage. A default value of100means a collection starts after the heap size doubles (e.g., grown by100%), while higher values lead to less frequent GC (using more memory) and lower values lead to more frequent GC (using less memory).
Sweeping and Allocation
After marking completes the sweep phase reclaims memory belonging to white objects. Sweeping is also incremental. It happens in the background and on allocation paths when needed. Because Go organizes memory into spans of fixed size classes, sweeping is efficient. Each span tracks its free slots so allocators can quickly grab space without scanning the whole heap.







