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.
*T distinguishes "not
set" from "zero value"). When none of those apply, prefer values.2 · Try it
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
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.
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
}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
| Form | Returns | Initialises to | Use for |
|---|---|---|---|
new(T) | *T | zero value | Rarely. Idiomatic Go uses &T{...} instead. |
&T{...} | *T | field values | Constructing pointers to structs, literals. |
make([]T, n) | []T | zero values × n | Slices, maps, channels — not pointers. |
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.Mutexor 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
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".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 / Python | Most objects are already implicit pointers. Go makes the pointer explicit; you choose value or reference. |
| Rust | Pointers without borrow-checking. No &mut vs & distinction — you mutate at will and the GC sorts it out. |
| JavaScript | Objects are reference types; primitives are values. Go just makes you write & and * explicitly so it's never ambiguous. |
9 · Common mistakes
- Dereferencing nil.
*nilpanics. Always checkif p != nilbefore 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() *Fooreturns*Foo, notFoo. The caller can choose to deref or pass the pointer along. - Treating
*Tas nullable for primitives. Sometimes right (Kubernetes-style optionality). Often a smell — prefer a wrapper type or "zero = absent" convention.
10 · Exercises (~10 min)
- Swap two ints. Write
func swap(a, b *int). Verify it changes the caller's variables. - Linked list node.
type Node struct { val int; next *Node }. Build a list of 3 nodes manually. Iterate by followingnextuntil nil. - 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. - Optional bool. Define
type Maybe[T any] struct { v T; set bool }— no pointer needed. Compare with*boolfor "unset vs false". Which is cleaner?
11 · When it clicks
- You reach for
&T{...}overnew(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 &localis safe — you know escape analysis handles it. - You use
*Tfor "optional primitive" only when you really need to distinguish "unset" from "zero".