Go Pointers
Overview
A pointer holds the memory address of a value rather than the value itself. Pointers let you share and mutate data across function boundaries without copying it. Go pointers are simpler than C pointers — no pointer arithmetic, and the garbage collector manages memory.
Pointer Basics
*T— the type "pointer to T"&x— address-of operator: produces a pointer tox*p— dereference operator: reads or writes the value at addressp- Zero value of a pointer:
nil
x := 42
p := &x // p is *int, holds the address of x
fmt.Println(p) // 0xc000018058 (some memory address)
fmt.Println(*p) // 42 — dereference: read value at address
*p = 100 // write through pointer
fmt.Println(x) // 100 — x was changed via p
Why Pointers?
Without pointer: function gets a copy — changes don't affect the original.
func doubleVal(n int) {
n = n * 2 // modifies local copy only
}
x := 5
doubleVal(x)
fmt.Println(x) // 5 — unchanged
With pointer: function operates on the original value.
func doublePtr(n *int) {
*n = *n * 2 // modifies value at the address
}
x := 5
doublePtr(&x)
fmt.Println(x) // 10 — changed!
Pointer to Struct
Go automatically dereferences struct pointers — p.Field is shorthand for (*p).Field. You almost never write (*p).Field explicitly.
type Point struct{ X, Y int }
p := &Point{X: 1, Y: 2}
p.X = 10 // Go handles the dereference: (*p).X = 10
fmt.Println(*p) // {10 2}
Value Receiver vs Pointer Receiver
The most common use of pointers in Go is pointer receivers on methods.
| Receiver | Modifies original? | When to use |
|---|---|---|
func (r Rect) Area() | No — copy | Read-only, small struct |
func (r *Rect) Scale() | Yes | Mutating method, or large struct |
type Counter struct{ count int }
// Value receiver — read-only
func (c Counter) Value() int { return c.count }
// Pointer receiver — mutates
func (c *Counter) Increment() { c.count++ }
func (c *Counter) Reset() { c.count = 0 }
c := Counter{}
c.Increment() // Go auto-takes &c for pointer receiver
c.Increment()
fmt.Println(c.Value()) // 2
Rule: If any method has a pointer receiver, use pointer receivers for all methods on that type. Mixing breaks interface satisfaction.
new() — Allocating Zero Values
new(T) allocates zero memory for type T and returns a *T. Rarely used — struct literals with & are more idiomatic.
p := new(int) // *int, *p == 0
*p = 42
fmt.Println(*p) // 42
// More idiomatic for structs
u := &User{Name: "Vatsal"} // preferred over new(User)
Stack vs Heap (Brief)
Go's compiler decides whether a variable lives on the stack (cheap, auto-freed) or heap (managed by GC):
- Variables that don't escape the function → stack
- Variables whose address is taken and returned/stored → heap (escape analysis)
You don't manage this manually. But knowing it helps explain why returning a pointer from a function is safe in Go (the heap keeps it alive), unlike C.
// Safe — Go detects the value escapes and allocates on heap
func newPoint(x, y int) *Point {
p := Point{X: x, Y: y} // allocated on heap because its address leaves this func
return &p // perfectly safe
}
Nil Pointer Dereference
Dereferencing a nil pointer causes a runtime panic. Always check for nil before dereferencing an optional pointer.
var p *int
fmt.Println(*p) // panic: runtime error: nil pointer dereference
// Safe pattern
if p != nil {
fmt.Println(*p)
}
Pointers in Function Signatures — Patterns
// 1. Mutate in place
func (u *User) SetEmail(email string) { u.Email = email }
// 2. Optional / nullable value (pointer = "may be absent")
func findUser(id int) *User {
if notFound { return nil }
return &User{...}
}
// 3. Large struct — avoid copying
func processReport(r *Report) { ... } // 100-field struct, no copy
// 4. Out parameter (rare — prefer multiple returns)
func parse(s string, out *int) error { ... }
When to Use Pointers
| Use pointer when | Use value when |
|---|---|
| Method needs to mutate the receiver | Read-only method |
| Struct is large (expensive to copy) | Primitive types (int, bool, string) |
| Representing optional / absent values | Small structs (Point, Color) |
| Sharing state across goroutines | Immutable data |
Gotchas
| Gotcha | Detail |
|---|---|
| Nil pointer dereference | Always check pointer is non-nil before using *p |
| Returning pointer to loop variable (Go < 1.22) | All pointers point to the same variable — capture with v := v inside the loop |
| Pointer to interface | Almost never needed — interfaces already hold a pointer internally |
*p = x vs p = &x | *p = x changes the value at p's address; p = &x makes p point to x — very different |