Collecting Garbage - Go (Part 1)
We create garbage, Go collects it, we will find out how.
Garbage collection is a topic I came across during my college days. I was curious about it and tried a lot to understand how different languages handle garbage collection, but I couldn’t get it at the time.
So, I am visiting again, this time better prepared.
What exactly is Garbage?
I didn’t start with context on purpose because, honestly, I didn’t have one either when I first heard the term.
You might ask, “What is Garbage Collection and what the hell is considered garbage?”.
Let’s understand. Consider the situation below -
You are vibe coding your way on your next SAAS, and since you want to get done with MVP fast, you are using Javascript (of course).
You're in the zone, shipping features fast. Objects are being created, variables declared left and right. And guess what?
You're not thinking about what memory is still being used and what isn’t.
But hey - your code works, and that’s all that matters… right?
Not to be blunt or anything, but you are creating garbage here. We all create garbage every day, even while writing our amazing structured code.
The variables you declared, the objects you created, when they are not used anymore are considered as garbage.
But why is that garbage? Because once you start getting traffic, you might see a drop in performance, memory leaks and all sorts of issues, and then you will have to revisit these.
If there is garbage, it needs to be collected, and that’s what modern programming languages do. But there are different ways of collecting garbage depending on the language.
To collect garbage, we need to know where garbage lives.
This will be the start of a series on Garbage Collection, starting with Go.
Stack vs Heap
If you’ve ever written a recursive function that didn’t stop, you’ve probably run into a “stack overflow” error.
If you’ve ever accidentally queried an entire table into memory, chances are you’ve seen something like “out of memory: heap limit exceeded.”
In Go, when you create a variable, the compiler decides where to allocate it - stack or heap - based on how it's used.
Stack Allocation
What it is: A region of memory used for function calls.
Fast: Stack allocation is super quick and automatically cleaned up when the function returns
Deallocates when the function returns - no garbage collection needed
func f1() {
x := 42 // likely allocated on stack
}Heap Allocation
What it is: A global memory pool managed by Go’s garbage collector.
Slower: Heap allocation is slower than stack because it needs GC and lifetime tracking.
Used when:
The object escapes the current function (used after the function exits).
It’s referenced by closures or stored in data structures that outlive the function.
func f1() *int {
x := 42
return &x // x escapes → allocated on heap
}So, the heap is where garbage lives, and this is where we will spend most of our time.
Collecting Garbage
When an application is running, the garbage and non-garbage elements live in the same space, in memory. To collect garbage, we need to make sure if something is garbage or not.
Classifying something as garbage is difficult; different algorithms are designed for it.
Mark-and-Sweep Garbage Collection
Garbage Collection in Go works by:
Marking: Finding out which objects are still "alive" (still used by my program).
Sweeping: Reclaiming memory used by objects that are no longer alive.
Tri-color Marking Algorithm
The tri-color method is a strategy to track what’s live and what’s not during the marking phase. It avoids issues like double-checking and infinite loops.
The 3 colors are:
White:
These are candidates for garbage - initially, all objects are white. If they are never visited during marking, they are collected.Grey:
These objects are reachable (like via a variable), but their contents haven’t been scanned yet. That means we know they’re live, but we haven’t checked what they reference.Black:
These are fully processed live objects - we’ve checked them and their references. They won't be garbage-collected.
How it works:
All objects start white.
The GC finds root objects (like global or stack variables) and makes them grey.
It scans grey objects one by one:
Marks them black.
If they reference other white objects, those get marked grey.
Only black (live) and white (garbage) objects remain when all grey objects are processed.
The white ones are freed.
This approach ensures that no live object is accidentally collected and no object is scanned twice.
After the cycle is done, we can see nodes - n8 and n9 are still white, which means they are the garbage! (and need to be collected).
So far, we've seen how a tricolor mark-and-sweep collector can identify garbage.
But there's a catch.
The Problem: Stop-the-World GC
In this model, the program must be paused while GC does its job - a "stop-the-world" pause.
During this time, nothing else runs - not the API calls, not the game loop, and not even the server response handlers.
For small heaps, these pauses may be unnoticeable.
But with large heaps or latency-sensitive applications, it becomes a big bottleneck!
The Solution: Concurrent Garbage Collection
To avoid freezing the application, modern runtimes like Go run the mark phase of the garbage collector concurrently with the program.
This means:
Your program continues to allocate memory and change pointers,
While the garbage collector is also running in the background.
Go's GC uses a concurrent tricolor marking algorithm, where it has to account for the fact that the program might move references during marking.
Tradeoff
While this avoids long pauses, it means:
GC and the program share CPU,
So throughput may reduce slightly in exchange for low latency.
Now that we know the mark phase is done concurrently, it could raise a serious problem. Consider the scenario below -
GC is currently traversing and marking nodes as black (reachable).
The program changes a pointer - for example, it makes a black node point to a white node (considered garbage).
That white node might never get marked because GC already scanned the black node.
This breaks our assumption - No black object should ever point to a white object.
This would mean reachable objects could be incorrectly collected as garbage!
The Solution: Write Barriers
To prevent this, Go uses write barriers - tiny pieces of code that run every time we write to a pointer.
Whenever a black object is about to point to a white object:
Go marks gray the white object immediately,
So that GC will visit and mark it.
This makes sure that all the new objects added to live objects are marked again.
To be continued…
And that wraps up Part 1 of this journey.
We started with a simple question - “What is garbage, really?” - and uncovered how modern languages like Go manage memory automatically.
From the idea of reachability to the tricolor marking algorithm, and finally the clever use of write barriers to avoid some concurrency bugs - we’ve covered quite a bit.
But there’s still more under the hood.
In the next part, we’ll look into what happens after marking - how languages deal with fragmented memory through compaction and why Go doesn’t even need it.
To make things even more interesting, we’ll try to code a basic garbage collector ourselves, just to see how it all ties together.
Stay tuned for Part 2 - things are about to get even more fun.





