08 - Pointers and Memory¶
What this session is¶
About an hour. Pointers are the topic that scares beginners the most and turns out to be small once you actually meet them. By the end of this page you'll understand what a pointer is, when to use one, and why methods sometimes have *Type in their receiver.
We will go slowly. Pointers feel weird the first three times.
The problem¶
When you pass a value to a function, Go gives the function its own copy. Changes the function makes don't affect the caller.
package main
import "fmt"
type Counter struct {
Value int
}
func increment(c Counter) {
c.Value = c.Value + 1
}
func main() {
counter := Counter{Value: 0}
increment(counter)
fmt.Println(counter.Value) // 0 - not 1!
}
Type and run. Surprise: the counter is still 0. Why?
increment received its own copy of the counter. It bumped that copy. The original is untouched.
For numbers this is fine - most functions don't want to mutate their inputs. But sometimes you do. You want increment(counter) to actually increment the counter. That's what pointers are for.
What a pointer is¶
A pointer is a value that stores the address of another value. Instead of being "the counter," a pointer is "where the counter lives."
Two operators:
&x- read as "the address of x." Gives you a pointer tox.*p- read as "the value pointed to by p." Goes from pointer to actual value. Called dereferencing.
package main
import "fmt"
func main() {
x := 42
p := &x // p is a pointer to x
fmt.Println(x) // 42
fmt.Println(p) // something like 0xc00001a098 - an address
fmt.Println(*p) // 42 - what p points to
*p = 100 // write through the pointer
fmt.Println(x) // 100 - x changed!
}
Type and run carefully. Notice the two important moves:
p := &xmadeppoint atx. Nowpknows wherexlives.*p = 100followed the pointer back toxand wrote100into it.xis now100.
The type of p is *int. Read it as "pointer to int." You'll see this notation in function signatures: func foo(n *int) means "foo takes a pointer to int."
Fixing the counter¶
package main
import "fmt"
type Counter struct {
Value int
}
func increment(c *Counter) {
c.Value = c.Value + 1
}
func main() {
counter := Counter{Value: 0}
increment(&counter)
fmt.Println(counter.Value) // 1!
}
Two changes:
func increment(c *Counter)- the parameter is now a pointer to aCounter, not a Counter.increment(&counter)- we pass the address ofcounter, not a copy.
Inside increment, c.Value = ... writes through the pointer to the original. The change sticks.
Notice: even though c is a pointer, we wrote c.Value, not (*c).Value. Go quietly does the dereferencing for field access. This is purely a convenience. Both forms work; the short one is what everyone writes.
Pointer receivers on methods (revisited from page 05)¶
Now the * on a method receiver makes sense:
This says: "the receiver is a pointer to a Counter, not a copy of one." When you call counter.Increment(), Go quietly passes the address of counter, the method mutates through it, the change sticks.
Compare to a value receiver:
This compiles, runs, and does nothing visible. A classic beginner bug.
The rule of thumb (stronger version of what page 05 said):
- If the method modifies the receiver's fields → use
*Type. - If the receiver is a big struct (lots of fields) → use
*Typeeven if you don't modify it. Saves the copy. - If the receiver is small and immutable → either works; pick one and be consistent across all methods on the type.
When in doubt, use *Type. Real Go code uses pointer receivers ~80% of the time.
nil pointers¶
A pointer can be nil - pointing at nothing. Dereferencing a nil pointer is a panic (crash).
var p *int // p is nil
fmt.Println(p == nil) // true
fmt.Println(*p) // PANIC: nil pointer dereference
You'll see this crash often when starting out. The fix is almost always "make sure the pointer is set to something before you use it." Either initialize it (p := &x) or check before dereferencing:
When to use pointers¶
The honest answer:
- A method needs to mutate its receiver. Use a pointer receiver.
- You're passing a large struct around and don't want to copy it every time. Use a pointer.
- You explicitly want a "nullable" value - a thing that might or might not be set. Use a pointer; check
nil. - You're sharing state between functions, and they all need to see updates. Use a pointer.
Otherwise: pass values around. Copying is cheap for small things. Go's compiler is good at avoiding actual copies when it safely can.
What about memory? Stack vs heap?¶
You may have heard programmers talk about "stack" and "heap." Quick story:
- The stack is fast memory used for function calls - every call grows it; every return shrinks it. Limited size.
- The heap is general-purpose memory that outlives any single function call. Garbage-collected (Go cleans up things you stop using).
Go decides for you. The compiler does escape analysis to figure out whether a value can safely live on the stack (faster) or must live on the heap (longer-lived). You almost never have to think about it.
The only thing you need to know now: taking the address of something with & may or may not cause it to live on the heap. Don't worry about it. Write clear code; let the compiler decide.
The full picture lands when you read "Go runtime" content later. For now: trust the language.
Exercise¶
In a new file account.go:
-
Define a struct
Accountwith two fields:Owner(string) andBalance(float64). -
Add a method
Deposit(amount float64)that addsamounttoBalance. Choose the right receiver type (value or pointer). -
Add a method
Withdraw(amount float64) errorthat: - Subtracts
amountfromBalanceifBalance >= amount. - Returns
nilon success. -
Returns an error like "insufficient funds: have X, want Y" otherwise.
-
In
main: -
Make sure both Deposit and Withdraw actually mutate
acct. (If they don't, you picked the wrong receiver type.)
What you might wonder¶
"Why don't I always use pointers, to be safe?"
Two reasons. (1) Pointers can be nil, which means an extra failure mode. (2) Pointers point at shared state, which means accidental mutation surprises. Values are simpler when simpler will do.
"Pointers in C scared me. Are Go pointers like that?"
A lot less scary. No pointer arithmetic (you can't p + 1 to "move along an array"). No manual free() - the garbage collector handles it. No casting between types. About 80% of what makes C pointers scary doesn't exist in Go.
"Is *Counter a different type from Counter?"
Yes, but Go is forgiving. You can call value-receiver methods on a pointer and vice versa most of the time. The type is technically distinct; the syntax is convenient.
"What's a **Counter?"
A pointer to a pointer. You'll see it rarely. Real-world reasons: a function that wants to replace what a pointer points to. Don't worry about it now; recognize it when you see it.
Done¶
You can now:
- Explain what a pointer is: a value holding the address of another value.
- Use &x to take an address and *p to dereference.
- Choose between value and pointer receivers on methods (mutation → pointer).
- Recognize and avoid nil pointer crashes.
- Stop fearing pointers.
You've now learned every basic mechanic of Go the language. From here on, the pages are about putting these mechanics together: concurrency (using all of them at once across multiple things running in parallel), testing (verifying your code does what you think), packages (organizing it), and eventually reading and contributing to real projects.
Next page: how Go does "do many things at once" - goroutines and channels.
Next: Concurrency 101 → 09-concurrency-101.md