Go Pointers: When, Why, and How to Use Them

The Value That Wouldn't Change

You write a function to update a user's name. You call it. You print the user. The name is unchanged. You stare at the code. The logic is correct. The function runs. But the change doesn't stick.
This is the classic "pass by value" confusion. Go copies values when passing to functions. Your function updated a copy. The original never knew. Understanding pointers is understanding when you want the original and when a copy is fine.

What Is a Pointer

A pointer is a variable that stores the memory address of another variable. Instead of holding a value directly, it holds the location where a value lives.
Go blog diagram 1

Go blog diagram 1

go
// Filename: pointer_basics.go package main import "fmt" func main() { name := "Alice" // & gets the address of a variable ptr := &name fmt.Println("Value:", name) fmt.Println("Address:", ptr) fmt.Println("Value at address:", *ptr) // * dereferences - gets value at address *ptr = "Bob" // Changes the original fmt.Println("After change:", name) }
Expected Output:
Value: Alice Address: 0x14000102050 Value at address: Alice After change: Bob

The Home Address Analogy

Think of it like this: When you give someone your home address instead of a photo of your house, they can visit and make changes. If you give them just a photo (a copy), they can look but can't change your actual house. A pointer is like giving the address. A value is like giving a copy.

Pass by Value: The Default

Go passes everything by value. When you call a function with a struct, the entire struct is copied.
go
// Filename: pass_by_value.go package main import "fmt" type User struct { Name string Email string } // This receives a COPY of the user func updateNameValue(u User, newName string) { u.Name = newName fmt.Println("Inside function:", u.Name) } func main() { user := User{Name: "Alice", Email: "alice@example.com"} updateNameValue(user, "Bob") // Original unchanged! fmt.Println("After function:", user.Name) }
Expected Output:
Inside function: Bob After function: Alice

Pass by Pointer: Modifying the Original

To modify the original, pass a pointer.
go
// Filename: pass_by_pointer.go package main import "fmt" type User struct { Name string Email string } // This receives a pointer to the user func updateNamePointer(u *User, newName string) { u.Name = newName // Go automatically dereferences fmt.Println("Inside function:", u.Name) } func main() { user := User{Name: "Alice", Email: "alice@example.com"} updateNamePointer(&user, "Bob") // Original changed! fmt.Println("After function:", user.Name) }
Expected Output:
Inside function: Bob After function: Bob
Go blog diagram 2

Go blog diagram 2

The *string Question: When to Use Pointer Types

The infamous *string. When should a field be *string instead of string?

Use Case 1: Optional Values (nil vs empty)

go
// Filename: optional_values.go package main import "fmt" // With string: empty and "not provided" are the same type ConfigBad struct { Host string Port int Timeout string // Is "" intentional or not set? } // With *string: nil means "not set", "" means "intentionally empty" type ConfigGood struct { Host string Port int Timeout *string // nil = use default, "" = explicitly empty } func printConfig(c ConfigGood) { if c.Timeout == nil { fmt.Println("Timeout: using default") } else if *c.Timeout == "" { fmt.Println("Timeout: explicitly disabled") } else { fmt.Println("Timeout:", *c.Timeout) } } func main() { // Timeout not specified c1 := ConfigGood{Host: "localhost", Port: 8080} printConfig(c1) // Timeout explicitly set timeout := "30s" c2 := ConfigGood{Host: "localhost", Port: 8080, Timeout: &timeout} printConfig(c2) }
Expected Output:
Timeout: using default Timeout: 30s

Use Case 2: JSON with omitempty

go
// Filename: json_optional.go package main import ( "encoding/json" "fmt" ) type User struct { Name string `json:"name"` Nickname *string `json:"nickname,omitempty"` // Won't appear if nil Age *int `json:"age,omitempty"` } func main() { // User without optional fields u1 := User{Name: "Alice"} data1, _ := json.Marshal(u1) fmt.Println("Without optionals:", string(data1)) // User with optional fields nickname := "Ally" age := 30 u2 := User{Name: "Alice", Nickname: &nickname, Age: &age} data2, _ := json.Marshal(u2) fmt.Println("With optionals:", string(data2)) }
Expected Output:
Without optionals: {"name":"Alice"} With optionals: {"name":"Alice","nickname":"Ally","age":30}

Performance: Pointer vs Value

Small Structs: Value is Often Faster

go
// Filename: small_struct_perf.go package main import "fmt" // Small struct (24 bytes on 64-bit) type Point struct { X, Y, Z float64 } // Value receiver - no indirection, cache friendly func (p Point) DistanceValue() float64 { return p.X*p.X + p.Y*p.Y + p.Z*p.Z } // Pointer receiver - indirection, potential cache miss func (p *Point) DistancePointer() float64 { return p.X*p.X + p.Y*p.Y + p.Z*p.Z }
For structs under ~64 bytes, value receivers often perform better because:
  • No heap allocation
  • No pointer dereferencing
  • Better cache locality

Large Structs: Pointer Avoids Copying

go
// Filename: large_struct_perf.go package main // Large struct (thousands of bytes) type BigData struct { Records [1000]Record Index map[string]int } type Record struct { ID int Data [256]byte } // WRONG: Copies entire struct on every call func ProcessValue(b BigData) { // 256KB+ copied each call! } // RIGHT: Only 8 bytes (pointer) passed func ProcessPointer(b *BigData) { // Fast, regardless of struct size }

Pointer Decision Guide

Go blog diagram 3

Go blog diagram 3

ScenarioUse PointerUse Value
Modify originalYesNo
Large struct (>64 bytes)YesNo
Optional field (nil matters)YesNo
Small struct, read onlyNoYes
Primitive types (int, bool)RarelyUsually
Slice, map, channelBuilt-in pointerN/A

Nil Pointer Danger

Dereferencing a nil pointer causes a panic.
go
// Filename: nil_pointer.go package main import "fmt" type User struct { Name string } func main() { var u *User // nil pointer // This panics! // fmt.Println(u.Name) // panic: runtime error: invalid memory address // Always check for nil if u != nil { fmt.Println(u.Name) } else { fmt.Println("User is nil") } // Or use a safe getter fmt.Println("Name:", getName(u)) } func getName(u *User) string { if u == nil { return "" } return u.Name }
Expected Output:
User is nil
Name:

Pointer vs Value Receivers

Choose receiver type based on these rules:
go
// Filename: receiver_types.go package main import "fmt" type Counter struct { count int } // Value receiver: doesn't modify original func (c Counter) GetCount() int { return c.count } // Pointer receiver: modifies original func (c *Counter) Increment() { c.count++ } // Pointer receiver: for consistency (other methods use pointer) func (c *Counter) Reset() { c.count = 0 } func main() { counter := Counter{} counter.Increment() counter.Increment() fmt.Println("Count:", counter.GetCount()) counter.Reset() fmt.Println("After reset:", counter.GetCount()) }
Expected Output:
Count: 2
After reset: 0

Rules for Receiver Choice

  1. If any method needs pointer receiver, use pointer for all methods (consistency)
  2. If method modifies receiver, use pointer
  3. If struct is large, use pointer (avoid copying)
  4. For small, immutable structs, value is fine

Memory and Escape Analysis

Go's compiler decides whether variables live on stack (fast) or heap (slower, needs GC).
go
// Filename: escape_analysis.go package main // Returns pointer to local variable - escapes to heap func createUserHeap() *User { u := User{Name: "Alice"} // Escapes to heap return &u } // Returns value - stays on stack func createUserStack() User { u := User{Name: "Bob"} // Stays on stack return u } type User struct { Name string }
Check with:
go build -gcflags="-m" main.go
./main.go:5: moved to heap: u
Implications:
  • Heap allocation requires garbage collection
  • Stack is faster (automatic cleanup)
  • Returning pointers forces heap allocation

Common Mistakes

Mistake 1: Modifying loop variable pointer
go
// WRONG: All point to same memory var users []*User for _, u := range userList { users = append(users, &u) // u is reused each iteration! } // RIGHT: Create new variable var users []*User for _, u := range userList { u := u // Shadow with local copy users = append(users, &u) }
Mistake 2: Nil pointer on optional struct field
go
// WRONG: Panics if Address is nil func PrintCity(u *User) { fmt.Println(u.Address.City) } // RIGHT: Check for nil func PrintCity(u *User) { if u != nil && u.Address != nil { fmt.Println(u.Address.City) } }
Mistake 3: Pointer to interface
go
// WRONG: Interface is already a pointer internally func process(r *io.Reader) { ... } // RIGHT: Interface directly func process(r io.Reader) { ... }

When NOT to Use Pointers

go
// Slices, maps, channels are reference types // They already point to underlying data // WRONG: Unnecessary pointer func processSlice(items *[]int) { ... } // RIGHT: Slice header is small, copy is cheap func processSlice(items []int) { ... } // Note: To grow the slice in function, you need pointer func appendItem(items *[]int, item int) { *items = append(*items, item) }

Summary: The Pointer Cheat Sheet

Go blog diagram 4

Go blog diagram 4

What You Learned

You now understand that:
  • Pointers store memory addresses: & gets address, * dereferences
  • Go passes by value: Functions get copies unless you use pointers
  • *string distinguishes nil from empty: Useful for optional fields
  • Small structs copy cheaply: Pointers aren't always faster
  • Large structs should use pointers: Avoid copying overhead
  • Nil pointers panic on dereference: Always check before accessing

Your Next Steps

  • Benchmark: Compare pointer vs value for your specific structs
  • Read Next: Learn about escape analysis with go build -gcflags="-m"
  • Practice: Refactor a function to use pointer receivers where appropriate
Pointers in Go are simpler than in C. No pointer arithmetic. Automatic dereferencing for struct fields. But you still need to understand when to use them. Now you know the rules. Apply them, and your code will be both correct and efficient.
All Blogs
Tags:golangpointersmemoryperformancebest-practices