My Journey with Pointers in Go: Value vs Pointer

When I first started with Go, I noticed something interesting: many developers seemed obsessed with pointers. Even for tiny structs, I'd see code like:

func NewUser(name *string, age *int) *User

I thought, "Okay, pointers must be the professional way to write Go." So I adopted this style and used pointers everywhere.

But over time, my code became messy: excessive nil checks, harder-to-read function signatures, and sometimes even worse performance. At that point, one of my seniors advised me to learn when pointers should be used and when they shouldn’t. That suggestion became the turning point — it pushed me to dig deeper into how pointers really work, and how they impact memory, performance, and code clarity.

What Pointers Really Are

A pointer is simply a memory address — a reference to where data lives, rather than the data itself. Instead of copying values, you can pass around this reference. This enables:

  • Sharing → Multiple functions can work on the same data.

  • Mutability → Functions can directly modify the caller’s values.

  • Optional values → Pointers can be nil, representing the absence of a value.

But pointers also come with trade-offs:

  • Extra indirection → The CPU must follow the reference to access the actual data.

  • Nil checks → You need to guard against nil to avoid runtime errors.

  • Heap allocation → Pointers often push data to the heap, increasing work for the garbage collector (GC).

Stack vs Heap: The Memory Story

Understanding pointers became much clearer when I learned about stack vs heap memory in Go:

Stack

  • Fast, automatically freed when the function returns

  • Used for small structs, primitives, etc., passed by value

  • No GC overhead

Heap

  • Slower, managed by the garbage collector

  • Occurs more frequently when using pointers and Go's escape analysis determines a variable must outlive the current function

  • More heap allocations = more GC work

💡 Passing a small struct by value usually stays on the stack (fast)

💡 Using pointers often forces Go to allocate on the heap (slower, more GC)

Understanding Escape Analysis

Go's compiler performs escape analysis to determine whether a variable can be safely allocated on the stack or must "escape" to the heap:


// Example 1: Stays on stack (no escape)
func getLength() int {
    s := "hello" // Allocated on stack
    return len(s)
}

// Example 2: Escapes to heap
func getString() *string {
    s := "hello" // Escapes to heap - returned pointer
    return &s
}

// Example 3: Might escape depending on usage
func processUser(u *User) {
    // If u is only used within this function, it may not escape
    u.Process()
}

You can view escape analysis results using:

go build -gcflags="-m" your_file.go

This will show which variables escape to the heap and why.

Understanding and Managing Escape Analysis

Escape analysis determines whether variables are allocated on the stack or heap. Here are common patterns that cause heap allocation:

  1. Returning Pointers to Local Variables - When you return the address of a local variable, it must escape to the heap to remain valid after the function returns

  2. Storing Pointers in Global or Package-Level Variables - Global variables have indefinite lifetime, forcing heap allocation

  3. Sending Pointers to Channels - The receiver may outlive the sender, requiring heap allocation

  4. Interface Method Calls on Values - Interfaces often require heap allocation due to unknown concrete type size

  5. Storing Pointers in Slices or Maps with Unknown Capacity - Dynamic collections with expanding capacity may force heap allocation

  6. Passing Pointers to Escaping Function Arguments - If function arguments escape, their pointed-to data may also escape

  7. Closures Capturing Local Variables by Reference - Variables captured by closures often escape to the heap

  8. Method Calls on Values Through Interfaces - The value may be copied to the heap to satisfy interface requirements

Practical decision guidelines:

ScenarioRecommendation
Small structsPass by value
Large structsPass by pointer
Need to mutate caller's valueUse pointer
Nil as valid optionUse pointer
Few primitive fieldsAvoid pointers

Benchmarking: Value vs Pointer

I didn’t just theorize—I ran benchmarks. I created structs of different sizes (from ~100 bytes up to 10MB) and tested two approaches: passing by value vs by pointer.


🔧 Test Setup

  • Structs of different sizes (TinyStruct, SmallStruct, MediumStruct, LargeStruct)

  • Functions that accept them either by value or by reference

// TinyStruct: ~100 bytes
type TinyStruct struct {
    data [25]int32
}

// Pass by value
func passTinyByValue(t TinyStruct) {
    t.data[0] = 42
}

// Pass by pointer
func passTinyByRef(t *TinyStruct) {
    t.data[0] = 42
}

func BenchmarkTinyByValue(b *testing.B) {
    t := TinyStruct{}
    for i := 0; i < b.N; i++ {
        passTinyByValue(t)
    }
}

func BenchmarkTinyByRef(b *testing.B) {
    t := &TinyStruct{}
    for i := 0; i < b.N; i++ {
        passTinyByRef(t)
    }
}

Full source code is available here:
GitHub Repository – Go Benchmarks: Value vs Pointer

The Results

Struct SizeValue PassingPointer PassingRecommendation
Tiny (<1KB)FastFastValues fine
Small (1KB-1MB)Slowing downFastDepends on use case
Medium (1MB-5MB)Very slowFastPointers better
Large (>5MB)ImpracticalFastAlways pointers

Why This Performance Difference?

The difference stems from copying cost:

  • Passing by value = copying the entire struct into function arguments

  • Passing by pointer = copying just an 8-byte address (constant-time, regardless of struct size)

My Final Takeaways

After all this learning (and mistakes 😅), here's how I approach pointers now:

  1. Start with values for small to medium-sized data

  2. Use pointers judiciously only when needed:

    • Large data structures

    • Required mutation semantics

    • Optional (nil-able) parameters

  3. Profile before optimizing - use go tool pprof to identify real bottlenecks

  4. Understand escape analysis - use -gcflags="-m" to see what goes to heap

This shift in mindset has simplified my Go code and improved performance where it truly matters.

That's my journey with pointers in Go. If you're like I was—using pointers everywhere—maybe this will help you see where they truly belong.

0
Subscribe to my newsletter

Read articles from Liton Chandra Shil directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Liton Chandra Shil
Liton Chandra Shil