— notes

Go Pointers and Memory Management

2 min read#go
On this page
  1. Understanding Stack and Heap Memory
  2. Passing by Value: The Default Behavior
  3. Introducing Pointers
  4. Escape Analysis
  5. Concurrency Issues – Data Race
  6. Key Takeaways

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

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.

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.

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.

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

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:

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