---
title: "Go Pointers and Memory Management"
date: 2024-11-17
tags: ["go"]
description: "A practical look at Go pointers, escape analysis, and how the runtime keeps memory both efficient and safe."
---

When I first started learning Go, I was intrigued by its approach to memory management. Go handles memory in a way that's both efficient and safe, but it can be a bit of a black box if you don't peek under the hood.

## Understanding Stack and Heap Memory

```mermaid
graph TD
    subgraph STACK["Goroutine Stack"]
        S1["Local variables"]
        S2["Function parameters"]
        S3["Return addresses"]
    end
    subgraph HEAP["Shared Heap"]
        H1["Escaped variables"]
        H2["Pointers returned\nfrom functions"]
        H3["Closure captures"]
    end
    EA{{"Escape Analysis\n(compile time)"}}
    EA -- "does not escape" --> STACK
    EA -- "escapes to heap" --> HEAP
```

## Passing by Value: The Default Behavior

In Go, when you pass variables like integer, string, or boolean to a function, they are passed by value. A copy is made.

```go
func increment(num int) {
    num++
}

func main() {
    n := 21
    increment(n)
    fmt.Println(n) // 21
}
```

## Introducing Pointers

To modify the original variable inside a function, pass a pointer.

```go
func incrementPointer(num *int) {
    (*num)++
}

func main() {
    n := 42
    incrementPointer(&n)
    fmt.Println(n) // 43
}
```

## Escape Analysis

Escape analysis determines whether variables need to live beyond their function scope.

```mermaid
flowchart TD
    A["Variable declared"] --> B{"Returned as\npointer?"}
    B -- Yes --> HEAP["Allocate on Heap"]
    B -- No --> C{"Captured by\nclosure?"}
    C -- Yes --> HEAP
    C -- No --> D{"Assigned to\ninterface?"}
    D -- Yes --> HEAP
    D -- No --> E{"Too large\nfor stack?"}
    E -- Yes --> HEAP
    E -- No --> STACK["Allocate on Stack"]
```

Run `go build -gcflags '-m'` on any Go file to see escape analysis decisions.

## Concurrency Issues -- Data Race

```go
func main() {
    var wg sync.WaitGroup
    counter := 0
    counterPtr := &counter
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            *counterPtr++
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", *counterPtr)
}
```

This has a data race. Fix with a mutex:

```go
var mu sync.Mutex
go func() {
    mu.Lock()
    *counterPtr++
    mu.Unlock()
    wg.Done()
}()
```

## Key Takeaways

- Passing by value is simple but can be inefficient for large data structures
- Using pointers avoids copying but requires synchronization for shared access
- Escape analysis determines stack vs heap allocation
- Go prevents dangling pointers via garbage collection
- Run `go test -race` to detect data races
