09 / 20 · Day 3
Day 3 · Concept 09

Pointers

Go has pointers — but not C's pointers. No arithmetic; can't increment a *T. The compiler runs escape analysis and decides whether a value lives on the stack or heap. You get explicit reference semantics without the manual memory management.


1 · The intuition

A pointer is the memory address of a value. In Go: p := &x takes a pointer to x; *p dereferences. The pointer type is *T (read "pointer to T"). Pointers and the values they reference are both garbage-collected — you don't manage memory.

Why have pointers at all? Three reasons: mutation (a callee changes the caller's data), sharing (avoid copying a 1KB struct on every call), optionality (a nil *T distinguishes "not set" from "zero value"). When none of those apply, prefer values.

2 · Try it

go main.go · the basics
package main

import "fmt"

func main() {
    x := 42
    p := &x         // p is a *int pointing at x
    fmt.Println(p)  // prints an address like 0xc000018030
    fmt.Println(*p) // 42 — the value at p

    *p = 99         // write through the pointer
    fmt.Println(x)  // 99

    // The zero value of any pointer is nil
    var q *int
    fmt.Println(q == nil)  // true

    // Dereferencing nil panics
    // fmt.Println(*q)  // panic: runtime error: invalid memory address
}

3 · Pointers in function arguments

go main.go · pass-by-pointer to mutate
package main

import "fmt"

func doubleVal(n int) { n *= 2 }     // works on a copy — no effect
func doublePtr(n *int) { *n *= 2 }   // mutates through the pointer

func main() {
    x := 5
    doubleVal(x)
    fmt.Println(x)  // 5 — unchanged

    doublePtr(&x)
    fmt.Println(x)  // 10
}

4 · Escape analysis — where does it live?

In C, returning &x from a function where x is local is undefined behaviour. In Go, the compiler does escape analysis: if x outlives the function, the compiler moves it to the heap automatically. The pointer is always safe.

go main.go · returning a pointer to a local
package main

import "fmt"

// Perfectly safe in Go. The compiler sees that &local escapes
// and moves the int to the heap.
func makeCounter() *int {
    local := 0
    return &local
}

func main() {
    a := makeCounter()
    b := makeCounter()

    *a = 5
    *b = 99
    fmt.Println(*a, *b)  // 5 99 — independent
}
See it with the toolchain. Run go build -gcflags="-m" main.go. The compiler prints: "moved to heap: local". You'll see the same on any struct that escapes through a return or a channel.

5 · new vs & vs make

FormReturnsInitialises toUse for
new(T)*Tzero valueRarely. Idiomatic Go uses &T{...} instead.
&T{...}*Tfield valuesConstructing pointers to structs, literals.
make([]T, n)[]Tzero values × nSlices, maps, channels — not pointers.
go main.go · all three side by side
package main

import "fmt"

type User struct{ Name string; Age int }

func main() {
    // 1. new — zero-value pointer
    p1 := new(int)
    fmt.Println(*p1)  // 0

    // 2. &T{...} — pointer with values
    p2 := &User{Name: "Alice", Age: 30}
    fmt.Println(p2)   // &{Alice 30}

    // 3. make — only for slice/map/chan
    s := make([]int, 3)
    fmt.Println(s)    // [0 0 0]
}

6 · Pointer or value for struct methods?

This is the daily choice. Use a pointer receiver when:

  • The method mutates the receiver.
  • The struct is large — say, more than a few words. Copying costs.
  • You want consistency with other methods on the same type (mixing breaks interface satisfaction).
  • The struct contains a sync.Mutex or other non-copyable field.

Use a value receiver when:

  • The receiver is a small fixed-size value (an int, two-word struct, etc.).
  • The method is conceptually read-only.
  • You want the type to be usable as a map key (only value-receiver methods don't preclude this).

7 · From the wild

go kubernetes · types showing pointer-for-optional (paraphrased)
type DeploymentSpec struct {
    Replicas *int32                  // pointer: nil means "use default"
    Selector *metav1.LabelSelector
    Template PodTemplateSpec          // value: always present
    Strategy DeploymentStrategy
    // ...
    Paused   bool                    // value: zero is meaningful (false = active)
    ProgressDeadlineSeconds *int32   // pointer: nil = no deadline
}

// Pattern: pointer-to-primitive when you need to distinguish
// "explicitly zero" from "not provided".
From the wild: github.com/kubernetes/kubernetes · Apache 2.0

Kubernetes uses pointers extensively for optional fields. A nil Replicas means "I didn't specify, use the default"; *int32 = 0 means "I explicitly want zero replicas". Without pointers, you can't tell the two apart.

8 · Coming from another language?

If you know…The bridge
C / C++Pointers, but no arithmetic. No void* — use any. No delete — the GC reclaims.
Java / PythonMost objects are already implicit pointers. Go makes the pointer explicit; you choose value or reference.
RustPointers without borrow-checking. No &mut vs & distinction — you mutate at will and the GC sorts it out.
JavaScriptObjects are reference types; primitives are values. Go just makes you write & and * explicitly so it's never ambiguous.

9 · Common mistakes

  • Dereferencing nil. *nil panics. Always check if p != nil before dereferencing an unknown-origin pointer.
  • Pointer to a slice element. Fine — until the slice grows and reallocates, then your pointer is stale. Don't hold pointers to slice elements across appends.
  • Pointer to a map value. Doesn't compile. Map values aren't addressable.
  • Returning a pointer expecting a value. func New() *Foo returns *Foo, not Foo. The caller can choose to deref or pass the pointer along.
  • Treating *T as nullable for primitives. Sometimes right (Kubernetes-style optionality). Often a smell — prefer a wrapper type or "zero = absent" convention.

10 · Exercises (~10 min)

  1. Swap two ints. Write func swap(a, b *int). Verify it changes the caller's variables.
  2. Linked list node. type Node struct { val int; next *Node }. Build a list of 3 nodes manually. Iterate by following next until nil.
  3. Escape analysis spy. Write a function that takes a struct value and returns a pointer to it. Compile with -gcflags="-m". Watch the compiler tell you the value escaped.
  4. Optional bool. Define type Maybe[T any] struct { v T; set bool } — no pointer needed. Compare with *bool for "unset vs false". Which is cleaner?

11 · When it clicks

  • You reach for &T{...} over new(T) by reflex.
  • You can predict whether a function will mutate the caller's data from the signature.
  • You don't worry about whether return &local is safe — you know escape analysis handles it.
  • You use *T for "optional primitive" only when you really need to distinguish "unset" from "zero".
Found this useful?