// MODULE 02

Pointers & Memory

Pointers demystified. They're not scary — they're just addresses. Let's make them click.

What Even Is a Pointer?

In Python, you never think about this. In Go, you sometimes do. Here's the deal:

A pointer is just a variable that holds a memory address instead of a direct value.

// Visual representation

Regular variable:

x := 42

┌─────────┐

│ 42 │ ← x holds the value directly

└─────────┘

Pointer variable:

p := &x

┌─────────┐ ┌─────────┐

│ 0xc0001 │ ──→ │ 42 │ ← p holds address, points to value

└─────────┘ └─────────┘

p x

The Two Operators

Pointer operators
x := 42

// & = "address of" — get the memory address
p := &x        // p is now a pointer to x
fmt.Println(p)  // 0xc0000b4008 (some memory address)

// * = "dereference" — get the value at address
fmt.Println(*p) // 42 (the actual value)

// Modify through pointer
*p = 100
fmt.Println(x)  // 100 — x changed!

Memory Trick: & = "get Address" (& looks like 'A')

* = "get value" (go *****through the pointer)

How Python Handles This

Python has pointers too — you just can't see them. Everything in Python is a reference.

Python (hidden references)
# Lists are mutable, passed by ref
def modify(lst):
    lst.append(4)

my_list = [1, 2, 3]
modify(my_list)
print(my_list)  # [1, 2, 3, 4] changed!

# Ints are immutable
def modify_int(x):
    x = 100

n = 42
modify_int(n)
print(n)  # 42 unchanged
Go (explicit choice)
// Slices work like Python lists
func modify(s []int) {
    s[0] = 999  // Modifies original
}

// Ints: pass by value (copy)
func modifyInt(x int) {
    x = 100  // Only changes copy
}

// Ints: pass by pointer (reference)
func modifyIntPtr(x *int) {
    *x = 100  // Changes original!
}

n := 42
modifyIntPtr(&n)  // Pass address
fmt.Println(n)     // 100

The difference: Go makes you choose explicitly. Python hides the decision based on mutability.

When to Use Pointers

Use Pointers When:

  1. You need to modify the original: Functions receive copies by default
  2. The data is large: Copying big structs is slow; pass a pointer instead
  3. You need to express "nothing": Pointers can be nil
  4. Consistency: If some methods need pointers, make all methods use pointers

Don't Use Pointers When:

  1. Data is small: int, bool, small structs — copying is fine
  2. You want immutability: Pass by value guarantees the original won't change
  3. Slices, maps, channels: Already reference types (sort of)
Practical examples
// ❌ Unnecessary pointer — int is tiny
func double(x *int) int {
    return *x * 2
}

// ✓ Just take the value
func double(x int) int {
    return x * 2
}

// ✓ Pointer needed — modifying a struct
func (u *User) UpdateEmail(email string) {
    u.Email = email
}

// ✓ Pointer useful — large struct
type BigData struct {
    Items [10000]int
}
func process(d *BigData) { /* ... */ }

nil: The Absence of a Value

A pointer that points to nothing is nil. It's Go's equivalent of Python's None for pointers.

nil pointers
var p *int           // Declared but not initialized
fmt.Println(p)       // <nil>
fmt.Println(p == nil) // true

// DANGER: Dereferencing nil crashes!
// fmt.Println(*p)  // panic: runtime error

// Always check for nil
if p != nil {
    fmt.Println(*p)
}

Using nil for "Optional" Values

nil as optional
type Config struct {
    Timeout *time.Duration  // nil = use default
    MaxSize *int             // nil = unlimited
}

func NewServer(cfg Config) *Server {
    timeout := 30 * time.Second  // default
    if cfg.Timeout != nil {
        timeout = *cfg.Timeout
    }
    // ...
}

// Usage
customTimeout := 60 * time.Second
NewServer(Config{Timeout: &customTimeout})
NewServer(Config{})  // Uses defaults

nil Panic: Dereferencing a nil pointer causes a panic (crash). Always check != nil before using *p.

Pointer Receivers on Methods

This is where pointers matter most in day-to-day Go.

Value vs Pointer receivers
type Counter struct {
    count int
}

// Value receiver: gets a COPY
func (c Counter) IncrementBroken() {
    c.count++  // Increments the copy, not original!
}

// Pointer receiver: gets the original
func (c *Counter) Increment() {
    c.count++  // Actually increments
}

func main() {
    c := Counter{}
    
    c.IncrementBroken()
    fmt.Println(c.count)  // 0 — didn't work!
    
    c.Increment()
    fmt.Println(c.count)  // 1 — works!
}

The Rule of Thumb

Go Does Some Magic: You can call pointer methods on values and vice versa -- Go automatically converts. But be consistent anyway.

Creating Pointers: new() and &

Different ways to create pointers
// Method 1: & operator (most common)
x := 42
p := &x

// Method 2: new() — allocates zeroed memory, returns pointer
p := new(int)      // *int pointing to 0
*p = 42

// Method 3: For structs, use & with literal
user := &User{Name: "Alice"}  // Returns *User

// Equivalent to:
user := new(User)
user.Name = "Alice"

new() vs make()

new vs make
// new() — for any type, returns pointer to zero value
p := new(int)        // *int → 0
s := new([]int)      // *[]int → nil slice (not useful!)

// make() — ONLY for slices, maps, channels
// Returns initialized (not pointer!) value
slice := make([]int, 10)        // []int with len=10
m := make(map[string]int)       // Initialized map
ch := make(chan int, 5)         // Buffered channel

Rule: new() = zero value + pointer. make() = initialized slice/map/channel (no pointer).

Common Pointer Mistakes

Mistake 1: Returning Address of Local Variable... is Fine!

This is actually okay in Go
func newUser() *User {
    u := User{Name: "Alice"</span|}
    return &u  // Fine! Go moves u to heap
}
// In C this would be a bug. Go's escape analysis handles it.

Mistake 2: Modifying Loop Variable

Loop variable trap
users := []User{{Name: "A"}, {Name: "B"}, {Name: "C"}}
var ptrs []*User

// ❌ WRONG: All pointers point to same address!
for _, u := range users {
    ptrs = append(ptrs, &u)  // &u is same address each iteration
}
// All ptrs[i] point to "C"!

// ✓ CORRECT: Use index
for i := range users {
    ptrs = append(ptrs, &users[i])
}

// ✓ OR in Go 1.22+: loop var is new each iteration

Mistake 3: Forgetting nil Check

Nil checks
func process(u *User) {
    // ❌ Crashes if u is nil
    fmt.Println(u.Name)
    
    // ✓ Check first
    if u == nil {
        return
    }
    fmt.Println(u.Name)
}

Exercises

Progress through each section in order, or jump to where you need practice.

Practice individual concepts you just learned.

💪 Challenges

Combine concepts and learn patterns. Each challenge has multiple variants at different difficulties.

Module 2 Summary

The Mantra: When in doubt, start without pointers. Add them when you need mutation or have large data.