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// 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
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
| Scenario | Use Pointer | Use Value |
|---|---|---|
| Modify original | Yes | No |
| Large struct (>64 bytes) | Yes | No |
| Optional field (nil matters) | Yes | No |
| Small struct, read only | No | Yes |
| Primitive types (int, bool) | Rarely | Usually |
| Slice, map, channel | Built-in pointer | N/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
- If any method needs pointer receiver, use pointer for all methods (consistency)
- If method modifies receiver, use pointer
- If struct is large, use pointer (avoid copying)
- 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
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.