Pointers, Gently
The problem
type Position struct {
Symbol string
Quantity float64
}
func (p Position) Add(qty float64) {
p.Quantity += qty
}
func main() {
pos := Position{Symbol: "BTCUSD", Quantity: 1.0}
pos.Add(0.5)
fmt.Println(pos.Quantity) // 1 - not 1.5
}No error. No warning. It silently does nothing. This is the single most common source of confusion for people learning Go, and it has one cause.
Everything is a copy
When you pass a value to a function or method, Go copies it. Add received a copy of pos, modified the copy, and threw it away.
main's pos Add's p (a separate copy)
┌──────────────┐ ┌──────────────┐
│ "BTCUSD" │ │ "BTCUSD" │
│ 1.0 │ │ 1.0 → 1.5 │ ← modified, then discarded
└──────────────┘ └──────────────┘See It Work: prove they're different
func (p Position) Add(qty float64) {
fmt.Printf(" inside: %p\n", &p)
p.Quantity += qty
}
func main() {
pos := Position{Symbol: "BTCUSD", Quantity: 1.0}
fmt.Printf("outside: %p\n", &pos)
pos.Add(0.5)
}outside: 0xc000010030
inside: 0xc000010048Different addresses. Two separate objects. The mystery evaporates the moment you can see it.
A pointer is an address
&x means "the address of x." *T is the type "pointer to T." *p means "the value at that address."
price := 100.0
p := &price // p is a *float64
fmt.Println(p) // 0xc000018030 - an address
fmt.Println(*p) // 100 - the value there
*p = 200.0 // write through the pointer
fmt.Println(price) // 200 - the original changed price p
┌───────┐ ┌──────────────┐
│ 200.0 │ ◄──────────── │ 0xc000018030 │
└───────┘ └──────────────┘
at 0xc000018030Fixing the method
Use a pointer receiver:
func (p *Position) Add(qty float64) {
p.Quantity += qty
}
pos := Position{Symbol: "BTCUSD", Quantity: 1.0}
pos.Add(0.5)
fmt.Println(pos.Quantity) // 1.5Now the method receives the address, and writes through it reach the original.
Note you still write pos.Add(0.5), not (&pos).Add(0.5) - Go inserts the & for you. Likewise p.Quantity works on a pointer without writing (*p).Quantity. Go removes almost all the punctuation that makes pointers painful in C.
Which receiver should I use?
The practical rules, in order:
- Need to modify the receiver? Pointer. No choice.
- Struct is large? Pointer, to avoid copying - the benchmark in Chapter 10 showed the cost.
- Small and read-only? Either works. Value is slightly clearer, because it can't surprise anyone.
- Be consistent within a type. If any method needs a pointer receiver, give them all pointer receivers. Mixing them is legal and confusing.
In practice most Go code uses pointer receivers for anything with methods that mutate, and value receivers for small immutable things.
Slices and maps are already sort of pointers
Here's the thing that trips people who've just learned the copy rule:
func modify(prices []float64) {
prices[0] = 999
}
func main() {
prices := []float64{100, 101, 102}
modify(prices)
fmt.Println(prices) // [999 101 102] - it changed!
}Wasn't the slice copied? Yes - but from Chapter 7 you know a slice is three fields, one of which is a pointer to the underlying array. Copying the slice header copies the pointer, and both copies point at the same array.
Maps behave the same way. So:
- Slices and maps - the contents are shared; a function can modify them.
- `append` is the exception - it may reallocate, so the caller won't see appended elements unless you return the slice.
- Structs, numbers, strings, booleans - genuinely copied.
nil, and the panic
A pointer that points at nothing is nil. Dereferencing it crashes:
var p *Position
fmt.Println(p.Quantity) // panic: invalid memory address or nil pointer dereferenceThat's Go's equivalent of the null-pointer error every language has. Guard when a pointer might legitimately be absent:
func describe(p *Position) string {
if p == nil {
return "no position"
}
return fmt.Sprintf("%s: %.4f", p.Symbol, p.Quantity)
}A nil pointer is genuinely useful for "this thing may not exist" - an open trade that might not be open. Just check before you reach through it.
See It Work: where does memory live?
Go decides for you whether a value lives on the stack (cheap, freed automatically when the function returns) or the heap (managed by the garbage collector). It's usually invisible, and you can ask:
go build -gcflags="-m" main.go./main.go:12:6: can inline newPosition
./main.go:13:9: &Position{...} escapes to heap
./main.go:20:15: pos does not escape"Escapes to heap" means the value outlived the function that made it - usually because a pointer to it was returned - so it couldn't live on the stack.
You don't need to optimise this. It's worth running once, on your own code, because it makes something completely invisible visible, and it explains why returning a pointer isn't automatically free.
A note on loop variables
There's a famous Go bug involving loop variables and closures. In Go 1.22 and later each iteration gets a fresh variable and the bug is gone. In older code you'll see this defensive line:
for _, bar := range bars {
bar := bar // looks pointless; wasn't, before Go 1.22
go process(&bar)
}If you see it in an old codebase, that's what it's for. You don't need to write it any more, but check what version a project targets before removing it.
Exercises
11.1 Take the broken Add above, prove the copy with %p, then fix it with a pointer receiver and prove the addresses now match.
11.2 Write Portfolio with a map of symbol to *Position, plus AddTrade, Close(symbol) and TotalValue(prices map[string]float64). Why is *Position in the map a better choice than Position?
11.3 Write two versions of a function that scales every price in a slice by a factor: one modifying in place, one returning a new slice. Show with %p that the second really is new.
11.4 Predict the output, then run it:
func f(s []float64) { s = append(s, 4) }
func g(s []float64) { s[0] = 99 }
func main() {
s := []float64{1, 2, 3}
f(s)
g(s)
fmt.Println(s)
}Explain the difference between the two.
11.5 Run go build -gcflags="-m" on your Chapter 10 code. Find one value that escapes to the heap and explain why.
11.6 Harder. Write a linked list of trades with Insert, Remove and Len. It's a poor structure for this job - but building one is the clearest way to understand pointers, because the structure is pointers.
Solutions
11.1 With a value receiver the addresses differ; with *Position they match, because no copy is made.
11.2 *Position lets you modify a position in place: p := portfolio.positions["BTCUSD"]; p.Quantity += 1 works. With Position values you'd get a copy and have to write it back, which is easy to forget.
11.4 Output is [99 2 3]. g modifies the shared array, so it sticks. f appends, which either reallocates or writes past the caller's length - either way the caller's len is still 3 and never sees it. This is the slice gotcha, and it's why append-style functions return the slice.
11.6 Each node holds a value and a *Node to the next. Removal means finding the node before the target and redirecting its pointer past it. Do it once with pencil and paper as well as code.