The Secret Life of Go: Memory Allocation
The Secret Life of Go: Memory Allocation
Slice internals, capacity, and the hidden memory leak
#Go #MemoryLeak #SoftwareEngineering #BackendDev
Eleanor is a senior software engineer. Ethan is her junior colleague. They work in a beautiful beaux arts library in Lower Manhattan — the kind of place where coding languages are discussed like poetry.
Episode 37
Ethan was monitoring the server dashboard, watching the RAM usage climb like a staircase until it hit the ceiling and the application crashed.
"I don't understand," Ethan muttered, digging into his performance profiler. "I built a log parser. It reads a one-gigabyte log file into memory, extracts just the ten lines where the crash happened, and returns them. The server shouldn't be holding onto a gigabyte of memory. It should only be holding a few kilobytes!"
Eleanor walked over, holding a freshly printed architecture diagram. "Show me the extraction function."
Ethan brought up the code:
// Ethan's log parser
func ExtractErrorBlock(massiveLogFile []byte) []byte {
// Search the 1GB file for the error marker
start := bytes.Index(massiveLogFile, []byte("FATAL"))
// Grab the next 500 bytes containing the stack trace
end := start + 500
// Return just the tiny 500-byte slice
return massiveLogFile[start:end]
}
"It looks perfectly harmless," Ethan said. "I slice the giant array, grab the 500 bytes I need, and let the 1GB massiveLogFile variable fall out of scope so the Garbage Collector can clean it up."
"It does look harmless," Eleanor agreed, taking a seat. "But the Garbage Collector is never going to clean up that gigabyte. You just created a massive memory leak."
The Slice Header and the Backing Array
Eleanor took a marker to the whiteboard. "In Go, a slice is not actually an array. A slice is just a 24-byte data structure called a Slice Header. It acts as a window looking into an array."
She drew a simple struct on the board:
type SliceHeader struct {
Data uintptr // Pointer to the actual underlying memory
Len int // How many items you are looking at
Cap int // How much total space the underlying array has
}
"When you load your 1GB log file, Go allocates a massive array on the Heap, and gives you a Slice Header pointing to the very beginning of it," Eleanor explained.
"And when I do massiveLogFile[start:end]?" Ethan asked.
"You aren't copying the data," Eleanor said. "You are just creating a new 24-byte Slice Header. It has a shorter Len, but its Data pointer is still pointing directly into the middle of that exact same 1GB backing array."
Ethan's eyes widened. "And because my new 500-byte slice is still pointing to the original array..."
"...the Garbage Collector cannot delete the array," Eleanor finished. "As long as even one single byte is being referenced by an active slice, the entire underlying backing array is kept alive in memory. You saved 500 bytes, but you kept a gigabyte hostage."
The Architect's Fix: Severing the Tie
"So how do I rescue my 500 bytes and let the gigabyte go away?" Ethan asked.
"You have to force Go to allocate a brand new, tiny backing array, and copy the data over," Eleanor said. "You must sever the tie to the original memory."
She showed him the modern Go solution using the standard library.
import (
"bytes"
"slices"
)
func ExtractErrorBlock(massiveLogFile []byte) []byte {
start := bytes.Index(massiveLogFile, []byte("FATAL"))
end := start + 500
// Extract the slice (still pointing to the 1GB array)
errorSlice := massiveLogFile[start:end]
// Clone it! This creates a brand new backing array
// and copies the 500 bytes over.
return slices.Clone(errorSlice)
}
"By returning the cloned slice, the only thing pointing to the 1GB backing array is the original massiveLogFile variable," Eleanor explained. "When this function exits, that variable disappears. The Garbage Collector sees that nothing is pointing to the 1GB array anymore, and sweeps it away."
Ethan ran his code again with the slices.Clone fix. The memory graph spiked when the file was loaded, and then instantly plummeted back to near zero as the Garbage Collector happily devoured the abandoned backing array.
"I see the problem now with my original code," Ethan said. "I kept a small slice that still pointed to the original 1GB backing array. The garbage collector couldn't free the memory."
"Exactly," Eleanor smiled. "When you only need a small piece of a massive dataset, clone the slice. Give it its own backing array."
Key Concepts Introduced
The Slice Header
- A slice in Go is not a standalone array. It is a lightweight, 24-byte struct (a "Slice Header") that contains a pointer to a backing array, the length (
Len) of the segment it can see, and the capacity (Cap) of the underlying array.
The Hidden Memory Leak
- When you take a sub-slice of an existing slice (e.g.,
small := massive[10:20]), Go does not copy the data. It creates a new Slice Header that points to the exact same backing array. - The Garbage Collector cannot free an array if any slice is still pointing to it. A 10-byte sub-slice can keep a 1-gigabyte backing array alive in memory forever.
Severing the Tie (Clone)
- To prevent this memory leak, you must copy the required data into a new, independent backing array.
- For slices of any type, use the modern standard
slices.Clone(s)to ensure the tie to the original backing array is completely severed.
Aaron Rose is a software engineer and technology writer at tech-reader.blog.
Catch up on the latest explainer videos, podcasts, and industry discussions below.
.jpeg)

Comments
Post a Comment