Go Memory Management — Junior Level¶
1. Introduction¶
What is it?¶
Go manages memory automatically: you don't malloc or free. The runtime allocates memory when you create a value (new, make, &T{}) and reclaims it via the garbage collector when nothing references it anymore.
p := &Point{X: 1, Y: 2} // Go allocates a Point
// ... use p ...
// When p goes out of scope and nothing references the Point,
// the GC reclaims its memory.
How to use it?¶
new(T): zero-initialized value, returns*T.make(...): for slices, maps, channels.&T{...}: literal + address, common idiom.
You usually don't think about WHERE memory lives; the runtime decides.
2. Prerequisites¶
- Pointers basics (2.7.1)
- Functions, slices, maps
3. Glossary¶
| Term | Definition |
|---|---|
| Stack | Per-goroutine memory, freed at function return |
| Heap | Shared memory, freed by GC |
| Garbage Collector (GC) | Runtime that reclaims unreferenced memory |
| Escape analysis | Compile-time decision: stack or heap |
| Allocation | Reserving memory for a value |
| Reference | A pointer or other indirect access keeping a value alive |
| Unreachable | A value with no references; eligible for GC |
4. Core Concepts¶
4.1 Stack vs Heap¶
- Stack: each goroutine has its own. Variables here die at function return. Fast.
- Heap: GC-managed. Variables live until no references remain. Slower allocation, requires GC.
The compiler decides per variable. You don't control directly, but you influence (e.g., returning &local forces heap).
4.2 Allocation Built-ins¶
// new(T) — zero-initialized, returns *T
p := new(int)
*p = 42
// make — for slices, maps, channels
s := make([]int, 5) // len=5
m := make(map[string]int)
ch := make(chan int, 10)
// &T{} — composite literal address
u := &User{Name: "Ada"}
4.3 Escape Analysis¶
The compiler decides where each variable lives:
func stays() {
n := 5
_ = &n // pointer doesn't escape; n on stack
}
func escapes() *int {
n := 5
return &n // n escapes; allocated on heap
}
Verify with go build -gcflags="-m".
4.4 Garbage Collection¶
The GC periodically scans the heap, finds reachable objects (from roots: stacks, globals), and frees the rest.
You don't trigger GC manually (usually); it runs automatically based on allocation rate.
4.5 Memory Lifetime¶
A value is alive as long as something references it (directly or transitively). When all references are dropped, it becomes garbage.
func makeUser() *User {
return &User{Name: "Ada"} // User on heap; alive while caller holds ptr
}
u := makeUser()
// u is alive
u = nil
// User is now garbage; GC will reclaim
5. Real-World Analogies¶
A self-cleaning room: in C, you must put away every toy yourself (free). In Go, the room cleans itself when you stop using a toy.
A library with auto-returns: you check out books (allocate); when you stop touching a book, the library auto-returns it (GC).
6. Mental Models¶
Stack (per goroutine):
[function frame 1: locals]
[function frame 2: locals]
...
↓ grows downward; freed at return
Heap (shared, GC'd):
[object 1]
[object 2]
...
↑ grows; objects reclaimed when unreachable
Roots (entry points for GC scanning):
- Goroutine stacks
- Global variables
7. Pros & Cons¶
Pros¶
- No manual memory management
- No use-after-free bugs
- No double-free
- Concurrent GC minimizes pauses
Cons¶
- GC overhead (CPU, occasional pauses)
- Heap allocation slower than stack
- Less control than manual allocation
- Need to understand escape analysis for performance
8. Use Cases¶
- Almost all Go programs (you don't usually opt out of GC).
new,make,&T{}for normal allocation.sync.Poolfor high-throughput allocation reuse.- Profile with pprof when GC pressure is high.
9. Code Examples¶
Example 1 — Stack-Allocated Local¶
Example 2 — Heap-Allocated via Escape¶
Example 3 — new¶
Example 4 — make for Slice¶
Example 5 — &T{...} Constructor¶
Example 6 — Verify Escape¶
// $ go build -gcflags="-m" main.go
// ./main.go:5:6: can inline foo
// ./main.go:6:9: &n escapes to heap
// ./main.go:6:9: moved to heap: n
Example 7 — Reduce Allocations¶
// Bad: allocates per call
for i := 0; i < 1000; i++ {
p := &Point{X: i, Y: i}
process(p)
}
// Better: reuse if possible
var p Point
for i := 0; i < 1000; i++ {
p = Point{X: i, Y: i}
process(&p)
}
10. Coding Patterns¶
Pattern 1 — Constructor¶
Pattern 2 — Pre-Allocate Slice¶
Pattern 3 — Pool Reuse¶
var pool = sync.Pool{New: func() any { return new(Buffer) }}
b := pool.Get().(*Buffer)
defer pool.Put(b)
Pattern 4 — Avoid Pointer Chains¶
Reduce pointer density to lower GC scan time.
11. Clean Code Guidelines¶
- Don't worry about allocation in non-hot code. Profile first.
- For hot paths, verify escape behavior with
-gcflags="-m". - Use
sync.Poolonly when measured allocation cost is high. - Pre-allocate slice/map sizes when known.
- Reduce pointer fields in hot data structures.
12. Product Use / Feature Example¶
A request handler with allocation awareness:
func handle(req *Request) *Response {
// Per-request allocation: response
resp := &Response{
Status: 200,
Body: process(req.Body),
}
return resp
}
If this is called 10k req/sec, that's 10k Response allocations/sec — typically fine. If profile shows GC pressure, use sync.Pool.
13. Error Handling¶
GC errors are rare. The runtime may crash with: - "out of memory" if heap exhausted (rare). - "stack overflow" if a goroutine's stack exceeds limit.
Both are abnormal; design for recovery via supervisors, not error returns.
14. Security Considerations¶
- Sensitive data stays in memory until GC reclaims it. Wipe after use:
- Cryptographic memory should not be reused via
sync.Poolwithout zeroing.
15. Performance Tips¶
- Stack > heap: avoid escape when possible.
- Pre-allocate slice/map capacity.
- Use
sync.Poolfor high-throughput allocations. - Reduce pointer density.
- Profile:
go test -bench -benchmem,pprof -alloc_space.
16. Metrics & Analytics¶
import "runtime"
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("Heap: %d MB; GC count: %d\n", ms.HeapAlloc/(1024*1024), ms.NumGC)
Useful for monitoring memory in production.
17. Best Practices¶
- Trust the GC for normal code.
- Profile before optimizing.
- Use
sync.Poolfor hot allocations. - Pre-allocate sizes.
- Verify escape with
-gcflags="-m".
18. Edge Cases & Pitfalls¶
Pitfall 1 — Sub-Slice Pinning¶
Fix: copy out.Pitfall 2 — Large Stack via Recursion¶
Goroutine stack max is 1 GiB. Deep recursion may overflow.
Pitfall 3 — Forgetting GC Doesn't Run on Demand¶
GC runs based on allocation rate; don't rely on runtime.GC() for prompt cleanup in normal code.
Pitfall 4 — Pool Doesn't Guarantee Retention¶
sync.Pool may discard at any GC. Don't rely on pool to hold state.
Pitfall 5 — Pointer Density Causes Long GC Pauses¶
1M *T is 1M GC roots. For high-throughput, prefer values.
19. Common Mistakes¶
| Mistake | Fix |
|---|---|
| Manually triggering GC | Trust the runtime |
| Sub-slice pins big array | Copy out |
sync.Pool for state retention | Pool is opportunistic |
| Excessive pointer fields | Use values when possible |
20. Common Misconceptions¶
1: "Go has no memory management." Truth: Go has automatic memory management — the GC.
2: "GC pauses are always bad." Truth: Modern Go GC pauses are typically <1ms; rarely a problem.
3: "I should call runtime.GC() to free memory." Truth: Almost never needed; the runtime decides.
4: "Stack allocation is always faster." Truth: For small values, yes. Heap is fine for normal code.
21. Tricky Points¶
- Escape analysis is compile-time; can't change at runtime.
makeandnewallocate;&T{}may or may not (depends on escape).sync.Poolis opportunistic — can be drained any time.- Goroutine stacks grow but don't shrink (until goroutine exits).
- GC scans pointer fields as roots; reduce them for performance.
22. Test¶
import "runtime"
import "testing"
func TestAllocation(t *testing.T) {
var ms1, ms2 runtime.MemStats
runtime.ReadMemStats(&ms1)
for i := 0; i < 1000; i++ {
_ = new(int)
}
runtime.ReadMemStats(&ms2)
if ms2.NumGC == ms1.NumGC {
t.Log("no GC ran")
}
}
23. Tricky Questions¶
Q1: Does this allocate?
A: No —n stays on stack; only the value is returned (in a register). Q2: Does this allocate?
A: Yes —n escapes to the heap. 24. Cheat Sheet¶
// Allocate
new(T) // *T, zero-init
make([]T, n) // slice
make(map[K]V, n) // map
make(chan T, n) // channel
&T{...} // composite literal
// Verify escape
go build -gcflags="-m"
// Pool reuse
var pool = sync.Pool{New: func() any { return new(T) }}
t := pool.Get().(*T); defer pool.Put(t)
// Stats
runtime.ReadMemStats(&ms)
25. Self-Assessment Checklist¶
- I understand stack vs heap
- I use new, make, &T{} correctly
- I know escape analysis basics
- I trust the GC
- I profile with pprof
- I use sync.Pool when measured
26. Summary¶
Go manages memory automatically. Stack: fast, function-scoped. Heap: GC-managed. Escape analysis decides. Use new, make, &T{...}. Trust the GC for normal code; profile and use sync.Pool for hot paths.
27. What You Can Build¶
- Servers handling millions of requests
- Long-running services
- High-throughput pipelines
- Memory-bound applications
28. Further Reading¶
29. Related Topics¶
- 2.7.1, 2.7.2, 2.7.3
- 2.7.4.1 Garbage Collection (next)
- Profiling chapter
30. Diagrams & Visual Aids¶
Stack vs Heap¶
Goroutine 1 stack: Goroutine 2 stack:
[main frame] [worker frame]
[foo frame] ...
[bar frame]
↓ freed at return
Heap (shared):
[allocated objects] ← GC scans, frees unreachable