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:
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
Storing Pointers in Global or Package-Level Variables - Global variables have indefinite lifetime, forcing heap allocation
Sending Pointers to Channels - The receiver may outlive the sender, requiring heap allocation
Interface Method Calls on Values - Interfaces often require heap allocation due to unknown concrete type size
Storing Pointers in Slices or Maps with Unknown Capacity - Dynamic collections with expanding capacity may force heap allocation
Passing Pointers to Escaping Function Arguments - If function arguments escape, their pointed-to data may also escape
Closures Capturing Local Variables by Reference - Variables captured by closures often escape to the heap
Method Calls on Values Through Interfaces - The value may be copied to the heap to satisfy interface requirements
Practical decision guidelines:
Scenario | Recommendation |
Small structs | Pass by value |
Large structs | Pass by pointer |
Need to mutate caller's value | Use pointer |
Nil as valid option | Use pointer |
Few primitive fields | Avoid 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 Size | Value Passing | Pointer Passing | Recommendation |
Tiny (<1KB) | Fast | Fast | Values fine |
Small (1KB-1MB) | Slowing down | Fast | Depends on use case |
Medium (1MB-5MB) | Very slow | Fast | Pointers better |
Large (>5MB) | Impractical | Fast | Always 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:
Start with values for small to medium-sized data
Use pointers judiciously only when needed:
Large data structures
Required mutation semantics
Optional (nil-able) parameters
Profile before optimizing - use go tool pprof to identify real bottlenecks
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.
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
