Stop the Leak! A Complete Guide to Memory Leaks in C++ and Go
Memory leaks are among the most common and frustrating issues in software development. They cause slowdowns, increasing resource usage, instability, and sometimes full application failure. This guide provides a clear, practical explanation of what memory leaks are, how they occur, how to detect them, and how to fix them in C++ and Golang.
What Is a Memory Leak?
A memory leak occurs when a program allocates memory but fails to release it when no longer needed.
Two main situations lead to leaks:
1. True leaks: Memory becomes unreachable and cannot be freed.
Example: calling malloc() in C/C++ but never calling free().
2. Logical leaks: Memory is still referenced but should not be.
Example: a cached map grows indefinitely, or a goroutine waits forever.
Get Sylvain Tiset’s stories in your inbox
Join Medium for free to get updates from this writer.
Both types result in increasing memory usage over time.
Memory Management Basics
Stack vs Heap
- Stack: automatic memory, freed when functions return
- Heap: dynamic memory, manually managed (C/C++) or garbage-collected (Go)
Memory life cycle
Regardless of the programming language, the memory life cycle is pretty much always the same:
- Allocate the memory needed
- Use the allocated memory
- Release the allocated memory when it is not required anymore
Manual vs Automatic Management
- C/C++: direct control (
new,delete,malloc,free) implies high risk of leaks - Go: garbage-collected implies fewer leaks, but leaks still occur through retained references or goroutines
Garbage collection removes unreachable memory, but it cannot guess when data should be discarded. As long as something references it, the GC must keep it.
How Memory Leaks Happen
Common Causes in C++
- Missing
deleteorfree()
void leak() {
int* p = new int(42);
// ...
// forgot: delete p;
}- Early returns or exception paths skipping deallocation
void leak(bool fail) {
int* p = new int(10);
if (fail) {
return; // 'p' is leaked on this branch
}
delete p;
}- Double allocation without freeing the previous pointer
void leak() {
int* p = new int(1);
p = new int(2); // previous allocation lost => leak
delete p;
}- Not following RAII (Resource Acquisition Is Initialization)
class FileWrapper {
public:
FILE* f;
FileWrapper(const char* path) { f = fopen(path, "w"); }
void writeSomething() { fprintf(f, "hello\n"); }
// missing fclose(f) in destructor => leak of file handle
};shared_ptrreference cycles (two objects owning each other)
#include <memory>
struct B; // forward declaration
struct A {
std::shared_ptr<B> b;
};
struct B {
std::shared_ptr<A> a;
};
void leak() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a; // cycle: use_count keeps both objects alive forever
}- Misuse of
new[],malloc, orrealloc
void leak() {
int* arr = new int[100];
// missing delete[] arr;
}
void leak() {
int* p = (int*)malloc(100);
delete p; // wrong deallocator → UB + leak
}
void leak() {
char* buf = (char*)malloc(10);
buf = (char*)realloc(buf, 20);
// If realloc fails, original buf is lost → leak
}Note: Resource Acquisition Is Initialization: a C++ design principle where a resource (such as memory, a file handle, a mutex, or a network connection) is acquired and released automatically through an object’s lifetime.
The idea is simple:
- A resource is acquired in the constructor of an object.
- The resource is automatically released in the object’s destructor when it goes out of scope.This ensures that resources are always cleaned up, even if a function returns early or an exception occurs. RAII eliminates the need for manual
deleteorfree()calls and is the foundation of safe memory and resource management in modern C++.
#include <cstdio>
#include <memory>
class File {
public:
File(const char* path, const char* mode) {
file_ = std::fopen(path, mode);
}
~File() {
if (file_) {
std::fclose(file_);
}
}
FILE* get() const { return file_; }
private:
FILE* file_;
};
void process() {
File f("data.txt", "r"); // resource acquired
FILE* file = f.get();
// ... do something ...
} // f goes out of scope → destructor automatically calls fclose()
// Note: This can also be achieved with smart pointers:
void example() {
auto buffer = std::make_unique<int[]>(100);
// memory is automatically freed when buffer goes out of scope
}Common Causes in Golang
- Goroutine leaks
func process(ch <-chan int) {
go func() {
for v := range ch {
// do something
}
}()
}- Channels that block forever
func producer(ch chan int) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
// no close(ch) → consumers block
}()
}- Slices retaining large underlying arrays
data := make([]byte, 10_000_000)
small := data[:10] // small slice holding reference to 10MB array- Keeping references in long-lived objects (like global caches)
var global []*bigStruct
func addLeak() {
s := make([]byte, 10<<20) // 10MB
global = append(global, s) // permanent retention
}var cache = map[string][]byte{}
func store(key string, data []byte) {
cache[key] = data // grows forever
}- Timers or tickers not stopped
ticker := time.NewTicker(time.Minute)
go func() {
for range ticker.C {
// work
}
}()- Contexts ignored or never cancelled
for i := 0; i < 1000; i++ {
// Using context.Background() means these contexts NEVER end.
worker(context.Background())
}Detecting Memory Leaks
General Signs of a Leak
These are general signs that indicate a potential memory leak:
- Gradually increasing RAM usage (Resident Set Size)
- GC running more frequently (in GC languages like Golang)
- Latency spikes
- Crashes or Out Of Memory (OOM) kills
- Production services getting slower over time
RSS (Resident Set Size) represents how much physical RAM a process is currently using. If a program leaks memory, its RSS tends to increase slowly over time, even when the workload stays the same. This happens because the application keeps allocating memory without releasing it, and the operating system cannot reclaim the leaked memory.
Virtual Memory on the other hand represents the memory a program believes it has.
C++ Tools
- Valgrind (memcheck): Detects memory leaks, invalid reads/writes, and improper memory usage by running the program in a monitored environment.
- AddressSanitizer (ASan): A fast compiler-based tool that catches memory errors such as buffer overflows, use-after-free, and out-of-bounds accesses.
- LeakSanitizer (LSan): Reports memory leaks at program exit by tracking allocations and their lifetimes, often used together with ASan.
- Heaptrack: Profiles memory allocations over time and identifies hotspots and growing memory patterns using visual graphs.
- Deleaker: A memory profiler for C++ that detects memory leaks, GDI and USER object leaks, and handle leaks. It provides a complete call stack for each allocation and integrates with popular IDEs, including Visual Studio, RAD Studio, and Qt Creator. Official website | Video tutorial
Golang Tools
- pprof: Go’s built-in profiler that collects heap, CPU, and goroutine profiles to identify where memory is allocated or retained.
- runtime.ReadMemStats: Provides detailed runtime memory statistics programmatically, useful for debugging or exporting custom metrics.
- GODEBUG=gctrace=1: Enables detailed garbage collector logs that reveal allocation patterns, GC frequency, and memory pressure.
- Grafana dashboards: Visualize memory metrics (heap usage, GC pauses, goroutines) from Prometheus or other exporters to spot leaks in production.
Fixing Memory Leaks in C++
C++ memory safety problems mainly come from manual management. The most robust approach is to avoid raw pointers as much as possible.
Apply RAII & Use Smart Pointers Correctly
RAII ensures that resources are freed when objects go out of scope.
std::unique_ptr<Foo> ptr = std::make_unique<Foo>();RAII and smart pointers avoids forgetting to delete memory. As a reminder, here are the three main smart pointers to be used:
- unique_ptr: For exclusive ownership. Prevents forgetting to free memory.
- shared_ptr: For shared ownership.
- weak_ptr: Breaks cycles.
More informations on smart pointers in this previous article.
Use tools
If it becomes hard to pinpoint a memory leak by hand, the best way forward is to use the tools mentioned earlier and start debugging with their help.
Fixing Memory Leaks in Golang
Go leaks are usually logical — the garbage collector cannot free memory that remains referenced or memory owned by stuck goroutines.
Goroutine Leaks
// Add context cancellation
func process(ctx context.Context, ch <-chan int) {
go func() {
for {
select { // Use select with a timeout
case v := <-ch:
// handle v
case <-ctx.Done():
return
}
}
}()
}
// Don't forget to close channels in the callers !Channels that block forever
func producerSafe(ch chan int) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // signals completion
}()
}Slice Backing Array Leaks
// Copy the needed part instead
small := append([]byte(nil), data[:10]...)Retaining References in Long-Lived Structures & Global Cache Leaks
var global []int // or another lightweight type
func addSafe() {
s := make([]byte, 10<<20)
checksum := int(s[0]) // or extract needed fields
global = append(global, checksum)
}var cache = make(map[string][]byte)
var cacheOrder []string
const maxEntries = 100
func storeSafe(key string, data []byte) {
if len(cache) >= maxEntries {
oldest := cacheOrder[0]
cacheOrder = cacheOrder[1:]
delete(cache, oldest)
}
cache[key] = data
cacheOrder = append(cacheOrder, key)
}Timers and Tickers Leaks
ticker.Stop()Contexts ignored or never cancelled
for i := 0; i < 1000; i++ {
ctx, cancel := context.WithCancel(context.Background())
worker(ctx)
time.AfterFunc(100*time.Millisecond, cancel) // guarantee cleanup
}Best practices
Adopting solid best practices greatly reduces the likelihood of memory leaks and makes them easier to track down when they appear.
Regular code reviews help catch risky patterns early, while load testing exposes leaks that only emerge under sustained or high-pressure workloads.
When hunting a leak, patience and a methodical, divide-and-conquer approach (dichotomy) remain essential to isolating the faulty component.
Keeping dependencies up to date is equally important, as libraries often ship memory-related fixes.
Finally, static analysis tools such as SonarQube can highlight suspicious constructs and provide early warnings for code that may leak.
In the end, tracking down memory leaks is one of the most difficult task for a computer engineer. It requires patience, persistence, and a bit of detective work.
Hoping memory leaks have no more secrets to you. If you liked this article, feel free to clap it, comment it or share it. To stay updated with my content, you can follow me on Medium.





