Address-of (&), dereference (*), and pass semantics.
Pointer Basics
A pointer is a value that holds the memory address of another value. Go
pointers are simpler than C's: you can't do pointer arithmetic, can't cast to
void*, can't accidentally read past the end. They're a tool for sharing and
mutating without copying.
& and *
&x— take the address ofx(produces a*T).*p— dereference the pointer (gives you back theT).
Pointers in functions
By default Go passes arguments by value — the function gets a copy. Pass a pointer if you want the function to mutate the original.
nil pointers
A pointer's zero value is nil. Dereferencing nil is a runtime panic
("invalid memory address"), so always check before dereferencing untrusted
pointers — usually the function that returns the pointer also returns an error
you should be checking first.
var p *int
if p != nil {
fmt.Println(*p)
}
new(T)
new(T) allocates zero-valued storage for a T and returns a pointer to it:
p := new(int) // *int pointing at a fresh 0
*p = 42
In practice new is rare. You'll more often write &MyStruct{...} which is
both shorter and lets you set fields at the same time.
Pointers vs references
Go has no & references like C++. Variables are either a value or a pointer —
not a reference. That said:
- Slices, maps, channels, functions, and interfaces internally contain pointers to a heap structure. So even when you pass them "by value", the underlying data is shared. That's why a function can mutate a slice's elements but not change its length as seen by the caller.
When should I use a pointer?
A rough rule of thumb:
- Use a pointer when the function needs to mutate the value, when the value
is large (avoid copying), or when
nilis a meaningful "not set" state. - Use a value for small, immutable things (an
int, a small struct, atime.Time). Copies are cheap and the code is simpler.
Don't reach for pointers just because you can — passing a small struct by value is often faster than chasing a pointer through memory.
Escape analysis
You'll hear about "stack vs heap" in Go. The good news: you almost never decide where memory lives. The compiler runs escape analysis and puts data on the heap only if it outlives the function (e.g., you return a pointer to a local variable — perfectly legal in Go, unlike C).
func newCounter() *int {
n := 0
return &n // n escapes to the heap — totally safe
}
Run go build -gcflags="-m" to see what the compiler decided.