Week 5: Pointers & Memory Basics

Week 3 introduced pointer receivers without fully explaining why they work the way they do. This week fills that gap — just enough of Go's memory model to reason confidently about what a function can and can't mutate, without needing to become a systems programmer to use it correctly.

Module 5 of 16 Week 5 of 16 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Use the & and * operators to take and follow a pointer
  • Explain why Go is always pass-by-value, even when it looks like pass-by-reference
  • Describe, at a practical level, the difference between the stack and the heap and what escape analysis decides
  • Explain what Go's garbage collector handles automatically, and what still requires care from the programmer

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.

the basics
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.

why this distinction matters
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.

a value that "escapes" to the heap
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 via defer.
  • 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

Hands-on

Build and diagnose a small pointer-based data structure

A linked list, built with pointers, plus a written explanation of its memory behavior.

Requirements:

  1. A Node struct with an int value and a Next *Node pointer, and a LinkedList struct wrapping a head pointer.
  2. A pointer-receiver method Push(value int) that adds a node to the front of the list.
  3. A method Sum() int that walks the list via pointers and returns the total.
  4. 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".
  5. A written (comment) explanation of what would happen, and why nothing crashes, if you removed all references to a Node from the list without explicitly freeing it.
Hint

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?

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?

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?

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?

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.