# Go Memory Optimization Guide

## Core Concepts

### Stack vs Heap
Go's compiler performs **escape analysis** to decide whether a variable lives on the stack (cheap, automatic cleanup) or heap (requires GC). The #1 memory optimization in Go is understanding and controlling escape behavior.

**Stack allocation**: Free (cleaned up when function returns)
**Heap allocation**: Costs ~25ns to allocate + GC overhead to scan and collect

### What causes escape to heap?
1. **Returning a pointer to a local variable**: `return &x`
2. **Storing in an interface**: `var i interface{} = x` (boxing)
3. **Sending to a channel**: `ch <- &x`
4. **Captured by a goroutine closure**: `go func() { use(x) }()`
5. **Too large for stack**: Very large arrays/structs
6. **Compiler can't prove lifetime**: Complex control flow

### Checking escape behavior
```bash
go build -gcflags='-m' ./...          # Basic escape info
go build -gcflags='-m -m' ./...       # Detailed reasons
go build -gcflags='-m' ./pkg/... 2>&1 | grep 'escapes to heap'
```

## pprof Heap Profiling

### Capture profiles
```bash
# During benchmarks (recommended)
go test -bench=. -memprofile=/tmp/mem.prof -benchmem -count=5 ./pkg/...

# From a running server (via net/http/pprof)
go tool pprof http://localhost:6060/debug/pprof/heap
```

### Analyze profiles
```bash
# Total bytes allocated (where memory is going)
go tool pprof -top -alloc_space /tmp/mem.prof

# Allocation count (GC pressure)
go tool pprof -top -alloc_objects /tmp/mem.prof

# Currently in-use (live objects, not freed)
go tool pprof -top -inuse_space /tmp/mem.prof

# Source-level annotation
go tool pprof -list=FunctionName /tmp/mem.prof
```

### Reading pprof output
```
      flat  flat%   sum%        cum   cum%
   512.5MB 35.2%  35.2%    512.5MB 35.2%  pkg.marshalResponse
   256.0MB 17.6%  52.8%    768.5MB 52.8%  pkg.handleRequest
```
- `flat`: Memory allocated directly in this function
- `cum`: Memory allocated by this function + everything it calls
- Focus on high `flat` (the function itself allocates) and high `cum-flat` gap (it calls something that allocates)

## GC Tuning

### GOGC (GC target percentage)
Controls how much the heap can grow before triggering GC.
- Default: `GOGC=100` (GC when heap doubles since last GC)
- Higher value: Less frequent GC, more memory usage
- Lower value: More frequent GC, less memory, more CPU in GC
- `GOGC=off`: Disable GC entirely (only for batch jobs with bounded memory)

### GOMEMLIMIT (Go 1.19+)
Sets a soft memory limit. GC becomes more aggressive as the limit approaches.
```bash
GOMEMLIMIT=1GiB ./server
```
Better than GOGC for memory-constrained environments because it adapts GC frequency to actual memory pressure.

### GC trace
```bash
GODEBUG=gctrace=1 go test -bench=. ./...
```
Output: `gc 1 @0.012s 2%: 0.11+1.2+0.034 ms clock, 0.89+0.45/1.1/0+0.27 ms cpu, 4->4->1 MB, 4 MB goal, 8 P`
- `2%`: Percentage of CPU spent in GC
- `4->4->1 MB`: Heap before GC → heap after GC → live data
- `4 MB goal`: Next GC target heap size

## Common Optimization Patterns

### 1. Return by value, not pointer
```go
// Before: escapes to heap
func newConfig() *Config {
    return &Config{Timeout: 30}
}

// After: stays on stack (if caller doesn't take address)
func newConfig() Config {
    return Config{Timeout: 30}
}
```

### 2. Pre-allocate slices and maps
```go
// Before: multiple grow operations
items := []Item{}
for _, raw := range data {
    items = append(items, parse(raw))
}

// After: single allocation
items := make([]Item, 0, len(data))
for _, raw := range data {
    items = append(items, parse(raw))
}
```

### 3. sync.Pool for frequently allocated objects
```go
var bufPool = sync.Pool{
    New: func() interface{} { return new(bytes.Buffer) },
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufPool.Put(buf)
    // use buf...
}
```

### 4. Avoid string/[]byte conversion
Each `string([]byte)` or `[]byte(string)` allocates a copy.
```go
// Before: two allocations
s := string(data)
result := []byte(s)

// After: work with []byte throughout, convert once at boundary
```

### 5. Use value receivers for small structs
```go
// Pointer receiver: struct escapes to heap if stored in interface
func (c *Config) String() string { ... }

// Value receiver: may stay on stack
func (c Config) String() string { ... }
```
Rule of thumb: If struct is ≤3 words (24 bytes on 64-bit), prefer value receiver unless you need mutation.

### 6. Struct field ordering (reduce padding)
```go
// Before: 24 bytes (with padding)
type Bad struct {
    a bool    // 1 byte + 7 padding
    b int64   // 8 bytes
    c bool    // 1 byte + 7 padding
}

// After: 16 bytes (no padding)
type Good struct {
    b int64   // 8 bytes
    a bool    // 1 byte
    c bool    // 1 byte + 6 padding
}
```

**Benchmark impact**: With 10M structs, the well-aligned version uses **80MB less memory** and runs ~4% faster. Use the `fieldalignment` linter to detect suboptimal layouts:
```bash
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
fieldalignment ./...
```

**Guidelines**: Order fields largest-to-smallest. Group same-size fields together. Avoid interleaving small and large fields.

#### False sharing in concurrent workloads

When multiple goroutines access different fields of the same struct on the same CPU cache line (64 bytes), writes to one field invalidate the other (false sharing). Fix with padding:

```go
// BAD: both fields on same cache line
type SharedCounterBad struct {
    a int64
    b int64
}

// GOOD: separate cache lines
type SharedCounterGood struct {
    a int64
    _ [56]byte // padding to next cache line
    b int64
}
```

**Benchmark**: Padding prevents false sharing, yielding ~3.8% improvement in tight concurrent loops.

### 7. Object pooling with sync.Pool

For hot-path objects that are frequently allocated and freed, `sync.Pool` reduces GC pressure dramatically:

```go
var itemPool = sync.Pool{
    New: func() interface{} { return new(Item) },
}

func process() {
    item := itemPool.Get().(*Item)
    defer itemPool.Put(item)
    *item = Item{} // reset before use
    // use item...
}
```

**Benchmark**: Object pooling achieves **20× throughput improvement** (42ns/op pooled vs 864ns/op fresh allocation) with 96% fewer allocations.

**Caveats**:
- Pool is drained on every GC cycle — don't rely on it for caching
- Always reset pooled objects before use
- Only pool objects that are allocation-heavy and short-lived

### 8. Reuse buffers across iterations
```go
// Before: new buffer each iteration
for _, item := range items {
    buf := new(bytes.Buffer)
    encode(buf, item)
    send(buf.Bytes())
}

// After: reuse buffer
buf := new(bytes.Buffer)
for _, item := range items {
    buf.Reset()
    encode(buf, item)
    send(buf.Bytes())
}
```

### 9. Buffered I/O for file and network writes

Unbuffered writes trigger a syscall per call. Buffered I/O batches writes, reducing syscalls dramatically:

```go
// Before: 10K syscalls (23.6ms/op)
for i := 0; i < 10000; i++ {
    f.Write([]byte("line\n"))
}

// After: ~3 syscalls (380µs/op — 62× faster)
buf := bufio.NewWriter(f)
for i := 0; i < 10000; i++ {
    buf.WriteString("line\n")
}
buf.Flush()
```

**Benchmark**: Buffered writes are **62× faster** (23.6ms → 380µs per operation). Default buffer is 4KB; use `bufio.NewWriterSize(f, 16*1024)` for high-throughput scenarios. Always call `Flush()` — `bufio.Writer` does NOT auto-flush on close.

### 10. Zero-copy techniques

Avoid copying data when you can reference the original:

```go
// mmap: map file into memory instead of Read+copy
data, err := syscall.Mmap(int(f.Fd()), 0, size, syscall.PROT_READ, syscall.MAP_SHARED)
defer syscall.Munmap(data)

// io.CopyBuffer: reuse a buffer for streaming
buf := make([]byte, 32*1024)
io.CopyBuffer(dst, src, buf)

// Slice sub-referencing instead of copy (careful with GC retention)
sub := original[start:end] // no allocation, shares backing array
```

**Benchmark**: mmap-based reads are **~2× faster** than `os.File.Read` for large files.

## Advanced GC Tuning

### GOMEMLIMIT (Go 1.19+)

Better than GOGC alone for memory-constrained environments. GC frequency adapts to actual memory pressure:

```bash
GOMEMLIMIT=1GiB ./server
```

**Combined tuning**: Set `GOGC=off` + `GOMEMLIMIT=1GiB` for batch jobs to minimize GC until the limit is approached.

### Weak References (Go 1.24+)

Go 1.24 introduced `weak.Pointer[T]` for caches that should not prevent GC:

```go
import "weak"

type Cache[K comparable, V any] struct {
    entries map[K]weak.Pointer[V]
}
```

Weak pointers become nil when their target is collected — useful for caches and deduplication tables that shouldn't hold objects alive.

### GC Behavior Thresholds

| GC Frequency | Meaning | Action |
|-------------|---------|--------|
| > 10/sec | Very high alloc rate | Reduce allocations (pre-alloc, pool, reuse) |
| 1-10/sec | Normal for servers | Acceptable unless latency-sensitive |
| < 1/sec | Low pressure | Good; check if GOGC is too high (wasting memory) |
| STW pause > 1ms | Concerning for latency | Reduce live pointer count, use value types |
