1. Pointers & the &/* Operators
A pointer holds the memory address of a value, not the value itself.
&x takes the address of x; *p
dereferences a pointer p to read or write the value it points at.
x := 10
p := &x // p is a *int, holding x's address
fmt.Println(*p) // 10 — dereferencing p gives x's value
*p = 20 // writing through the pointer changes x
fmt.Println(x) // 20
Go has no pointer arithmetic (no p++ to walk memory like in C) — a
pointer can only be taken, dereferenced, and passed around, which removes an entire
category of memory-corruption bugs by design.
2. Go Is Always Pass-by-Value
Every function argument in Go is copied — including a pointer itself. Passing a pointer doesn't change that rule; it just means the value being copied is an address, and following that address reaches the original data.
func double(n int) {
n = n * 2 // mutates the local copy only
}
func doublePtr(n *int) {
*n = *n * 2 // dereferences the copied pointer to mutate the original
}
func main() {
x := 5
double(x)
fmt.Println(x) // 5 — unchanged
doublePtr(&x)
fmt.Println(x) // 10 — changed, via the pointer
}
This is exactly why Week 3's SetEmail needed a pointer receiver — a
method with a value receiver is really just a function taking a copy of the struct,
same as double above. Slices, maps and channels behave as if passed by
reference in practice, but that's because the value being copied (a small header
containing a pointer to the real data) is cheap to copy and still points at the same
underlying storage — the pass-by-value rule never actually breaks.
3. Stack vs. Heap & Escape Analysis
The stack is fast, function-scoped memory that's automatically reclaimed the moment a function returns. The heap is longer-lived memory that has to be tracked and eventually freed by the garbage collector. Go's compiler decides which one a given value lives in — you don't choose explicitly.
func newUser(name string) *User {
u := User{Name: name} // looks stack-local...
return &u // ...but its address escapes the function
}
Because &u is returned, u can't safely live on
newUser's stack frame — that frame is gone the instant the function
returns. The compiler's escape analysis detects this and allocates
u on the heap instead, automatically. You can see this decision for any
file with go build -gcflags="-m" — worth running once out of curiosity,
though tuning around escape analysis is a Week 15 performance concern, not a Week 5
one.
4. Garbage Collection: What Go Handles
Go's garbage collector automatically frees heap memory once nothing references it
anymore — there's no malloc/free, no manual
delete, and (unlike C++) no risk of a dangling pointer to memory that
was already freed.
What it doesn't do for you:
- Close resources like files, network connections, or database handles — those still need an explicit
Close(), typically viadefer. - Prevent a goroutine leak — a goroutine blocked forever (say, on a channel nobody will ever send to) is never collected, because it's still technically running and reachable.
- Guarantee when collection happens — it runs on its own schedule, not deterministically the instant a value becomes unreachable.
In practice, this means Go frees you from manual memory bookkeeping almost entirely, while leaving resource cleanup and goroutine lifecycle firmly your responsibility — a theme that returns directly in Week 6 and Week 7.
5. Hands-on Exercise
Build and diagnose a small pointer-based data structure
A linked list, built with pointers, plus a written explanation of its memory behavior.
Requirements:
- A
Nodestruct with anintvalue and aNext *Nodepointer, and aLinkedListstruct wrapping a head pointer. - A pointer-receiver method
Push(value int)that adds a node to the front of the list. - A method
Sum() intthat walks the list via pointers and returns the total. - Write a short comment identifying which values in your program plausibly escape to the heap, and why — confirm your reasoning against
go build -gcflags="-m". - A written (comment) explanation of what would happen, and why nothing crashes, if you removed all references to a
Nodefrom the list without explicitly freeing it.
If go build -gcflags="-m" reports more values escaping than you expected, remember the rule isn't just "did I return a pointer" — a value also escapes if it's stored somewhere the compiler can't prove is stack-local, like inside a struct field on a struct that itself escapes, which is exactly what happens to every Node a LinkedList holds onto.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does calling double(x) leave x unchanged, while doublePtr(&x) changes it — given that both arguments are technically copied?
Why does calling double(x) leave x unchanged, while doublePtr(&x) changes it — given that both arguments are technically copied?
double receives a copy of the integer itself, so mutating its local parameter has no effect on the caller's x. doublePtr receives a copy of x's address — the copy is still just an address, but dereferencing it with *n reaches and mutates the one real x that both the copy and the original point at.
Q2
Why is Go still accurately described as "always pass-by-value" even though passing a slice to a function lets that function's changes to its elements be visible to the caller?
Why is Go still accurately described as "always pass-by-value" even though passing a slice to a function lets that function's changes to its elements be visible to the caller?
What gets copied when a slice is passed is a small header (a pointer to the backing array, a length, and a capacity) — that header itself is passed by value, but the pointer inside it still points at the same backing array as the original. Mutating an element through that shared backing array is visible to the caller, but the copying itself never violates pass-by-value; it's exactly the same mechanism as passing a pointer directly.
Q3
In the newUser example, why can't the local variable u simply stay on the stack, and what decides that?
In the newUser example, why can't the local variable u simply stay on the stack, and what decides that?
The function returns &u, so a reference to u outlives the function call that created it — but a stack frame is reclaimed the instant its function returns, which would leave that returned pointer dangling. The compiler's escape analysis detects that u's address escapes the function and allocates it on the heap instead, where it can safely outlive the call.
Q4
Why can a goroutine blocked forever on an empty channel still cause a memory leak, even though Go has garbage collection?
Why can a goroutine blocked forever on an empty channel still cause a memory leak, even though Go has garbage collection?
The garbage collector only reclaims memory that's unreachable — a goroutine stuck waiting on a channel is still running and still holds live references to whatever it captured, so none of that memory (or the goroutine's own stack) is ever considered garbage. Garbage collection has nothing to do with the goroutine itself still being alive; that's a lifecycle problem the programmer has to prevent directly, typically with the context cancellation patterns covered in Week 7.