Back to Notes

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 to x
  • *p — dereference operator: reads or writes the value at address p
  • 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.

ReceiverModifies original?When to use
func (r Rect) Area()No — copyRead-only, small struct
func (r *Rect) Scale()YesMutating 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 whenUse value when
Method needs to mutate the receiverRead-only method
Struct is large (expensive to copy)Primitive types (int, bool, string)
Representing optional / absent valuesSmall structs (Point, Color)
Sharing state across goroutinesImmutable data

Gotchas

GotchaDetail
Nil pointer dereferenceAlways 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 interfaceAlmost 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