Go Pointers with Maps & Slices — Junior Level¶
1. Introduction¶
Slices and maps in Go are "reference-like" types: when you pass them to a function, the small header is copied, but the underlying data is shared. Pointers interact with these types in specific ways:
- Slice elements ARE addressable:
&s[0]works. - Map values are NOT addressable:
&m["key"]is a compile error. - For mutable struct values in a map, store pointers:
map[K]*V.
s := []int{1, 2, 3}
p := &s[0]
*p = 99
fmt.Println(s) // [99 2 3]
m := map[string]int{"a": 1}
// p := &m["a"] // ERROR
2. Prerequisites¶
- Pointers basics (2.7.1)
- Slices and maps (2.3)
- Call by value (2.6.7)
3. Glossary¶
| Term | Definition |
|---|---|
| Slice header | Three-word value: array pointer, length, capacity |
| Backing array | The contiguous memory holding slice elements |
| Map handle | Pointer to internal hash table |
| Addressable | Can have its address taken via & |
| Aliasing | Sharing the same underlying data through multiple references |
4. Core Concepts¶
4.1 Slice Header Is Copied; Backing Array Is Shared¶
The function got a copy of the slice header, but the array it points to is the same.
4.2 Slice Element Pointers¶
Valid because slice elements are addressable.
4.3 Map Values Cannot Be Addressed¶
Workaround: extract, modify, restore:
4.4 Map of Pointers for Mutable Values¶
type Counter struct{ N int }
m := map[string]*Counter{"a": {N: 1}}
m["a"].N++
fmt.Println(m["a"].N) // 2
Storing pointers gives you addressable targets through dereference.
4.5 Pointer to Slice (Rare)¶
Use only when you need to REASSIGN the caller's slice.
4.6 Stale Pointers After Append¶
s := make([]int, 3, 3) // cap = 3
p := &s[0]
s = append(s, 99) // realloc; new array
*p = 999 // modifies OLD array
fmt.Println(s) // [1 0 0 99] — *p doesn't affect s
When append exceeds capacity, the backing array is reallocated. Existing pointers become stale.
5. Real-World Analogies¶
A library card and the bookshelf: the slice is your card; the bookshelf is the backing array. Many cards can point to the same shelf. You can also have a finger pointing at a specific book (&s[i]). But if the library reorganizes shelves (append realloc), your finger now points at the OLD shelf — useless.
Mailbox vs the address book: map's value isn't a fixed mailbox you can hand over — it's an entry in a dynamic register that may be rehashed. To mutate a value, you take it out, modify, put it back.
6. Mental Models¶
Slice s passed to function:
header (caller) header (callee)
[arr|len|cap] →copy→ [arr|len|cap]
│ │
└──────► backing array ◄─┘
(shared!)
For mutating elements: works through shared array. For appending: callee may reallocate; caller's header unchanged.
7. Pros & Cons¶
Pros (Slices)¶
- Element pointers are valid and useful
- Element mutation propagates
- Cheap to pass (just header)
Cons¶
- Aliasing surprises
- Stale pointers after append realloc
- Slice header reassignment doesn't propagate
Pros (Maps)¶
- Mutation propagates (handle is shared)
- O(1) lookup
Cons (Maps)¶
- Values not addressable
- Need pointers in values for mutation
- Slow iteration (no order guarantee)
8. Use Cases¶
- Mutating slice elements via pointer.
- Storing pointers in slices for shared data.
- Map of pointers for mutable struct values.
- Pointer-to-slice for header reassignment in functions.
- Indexing through pointer for sub-iteration.
9. Code Examples¶
Example 1 — Slice Element Pointer¶
Example 2 — Map of Pointers¶
type Counter struct{ N int }
m := map[string]*Counter{
"a": {N: 0},
"b": {N: 0},
}
m["a"].N++
m["a"].N++
m["b"].N++
fmt.Println(m["a"].N, m["b"].N) // 2 1
Example 3 — Slice of Pointers¶
type User struct{ Name string }
users := []*User{
{Name: "Ada"},
{Name: "Bob"},
}
for _, u := range users {
u.Name = strings.ToUpper(u.Name)
}
fmt.Println(users[0].Name) // ADA
Example 4 — Reassign Slice Through Pointer¶
func appendAll(sp *[]int, values ...int) {
*sp = append(*sp, values...)
}
s := []int{1, 2}
appendAll(&s, 3, 4, 5)
fmt.Println(s) // [1 2 3 4 5]
Example 5 — Stale Pointer Demo¶
s := make([]int, 3, 3)
s[0] = 1; s[1] = 2; s[2] = 3
p := &s[0]
fmt.Println(*p) // 1
s = append(s, 99) // exceeds cap; new backing array
*p = 999 // modifies the OLD array
fmt.Println(s) // [1 2 3 99] — *p not reflected
fmt.Println(*p) // 999 — refers to detached old array
Example 6 — Map Value Extract-Modify-Restore¶
type Counter struct{ N int }
m := map[string]Counter{"a": {N: 1}}
c := m["a"]
c.N++
m["a"] = c
fmt.Println(m["a"].N) // 2
Example 7 — Iterating Slice With Element Pointer¶
10. Coding Patterns¶
Pattern 1 — Map of Pointers for Mutability¶
Pattern 2 — Slice of Pointers for Shared/Heterogeneous¶
Pattern 3 — Pointer-to-Slice for Append-In-Function¶
Pattern 4 — Index Pointer in Loop¶
11. Clean Code Guidelines¶
- Slice elements: prefer indexing
s[i]for clarity; pointer&s[i]only when needed. - Map values: use
map[K]*Vwhen V needs mutation. - Avoid storing pointers to slice elements past appends — they may go stale.
- Prefer return-new-slice over
*[]Tparameters for clarity. - Defensive copy when storing caller-provided slices.
12. Product Use / Feature Example¶
A scoreboard (mutable map values):
type Score struct{ Points int }
scoreboard := map[string]*Score{
"alice": {Points: 0},
"bob": {Points: 0},
}
func awardPoints(player string, points int) {
if s, ok := scoreboard[player]; ok {
s.Points += points
}
}
awardPoints("alice", 10)
awardPoints("alice", 5)
awardPoints("bob", 3)
fmt.Println(scoreboard["alice"].Points) // 15
*Score enables direct mutation; with Score value, you'd need extract-modify-restore.
13. Error Handling¶
m := map[string]*Counter{}
c, ok := m["a"]
if !ok {
return fmt.Errorf("counter not found")
}
if c == nil {
return fmt.Errorf("nil counter")
}
c.N++
Map lookup returns the zero value (nil for pointer types) for missing keys; check before dereferencing.
14. Security Considerations¶
- Slices passed to functions are aliased — caller can mutate after callee returns.
- Defensive copy for caller-provided slices stored long-term.
- Map of pointers exposes mutable internals to anyone with access to the map.
- Stale pointers after append can lead to "lost" updates — be aware.
15. Performance Tips¶
- Slice headers are 24 B — passing them is essentially free.
- Map values inline when value type is small; use pointers for large values to keep buckets compact.
- Pre-allocate slice cap to avoid append reallocations.
- Pre-allocate map size with
make(map[K]V, n)if you know roughly N entries.
16. Metrics & Analytics¶
For ints in maps, no pointer needed — increment via index works (Go handles it specially for additive updates of map values).
Wait — actually m[k]++ is m[k] = m[k] + 1, so it requires re-storing. That works for map[K]int.
17. Best Practices¶
- Slice element pointers OK for mutation.
- Map of pointers for mutable struct values.
- Avoid storing pointers across append-realloc.
- Document aliasing in function signatures.
- Use
[]T(slice) for sequential data;[]*T(slice of pointers) when sharing or polymorphism is needed.
18. Edge Cases & Pitfalls¶
Pitfall 1 — &m[k] Compile Error¶
Workaround: extract. Pitfall 2 — Stale Element Pointer¶
Pitfall 3 — Map Value Field Mutation¶
Pitfall 4 — Nil Map Write¶
Initialize withmake. Pitfall 5 — Append Doesn't Propagate¶
Return the new slice, or use*[]T. 19. Common Mistakes¶
| Mistake | Fix |
|---|---|
&m[k] | Extract first |
m[k].Field = v for value-typed V | Store as *V or extract |
| Pointer to slice element after append | Refresh after append |
| Forgetting nil map init | make(map[K]V) |
| Aliasing surprise on slice store | Defensive copy |
20. Common Misconceptions¶
1: "Slices are passed by reference." Truth: Header is passed by value; backing array shared. Subtle but important.
2: "I can take the address of a map value." Truth: No. Map values are not addressable.
3: "After append, my old slice still has the appended elements." Truth: If reallocation occurred, the old slice header still has the old array (and old length). Always reassign: s = append(s, ...).
4: "Pointers to slice elements are always safe." Truth: Stale after reallocation.
21. Tricky Points¶
- Slice elements addressable; map values not.
- Append may or may not reallocate (depends on capacity).
s = append(s, ...)is required to capture potential reallocation.- Map of pointers vs map of values has very different mutation semantics.
- Slice of pointers vs slice of values affects GC pointer density.
22. Test¶
func TestSliceMutation(t *testing.T) {
s := []int{1, 2, 3}
p := &s[0]
*p = 99
if s[0] != 99 { t.Fail() }
}
func TestMapPointerMutation(t *testing.T) {
type C struct{ N int }
m := map[string]*C{"a": {}}
m["a"].N = 42
if m["a"].N != 42 { t.Fail() }
}
23. Tricky Questions¶
Q1: What does this print?
A:1. v is a copy; modifying it doesn't affect the map. Q2: What does this print?
A:[1 0 0 99]. Append reallocated; s points to new array; *p modified the old (now-detached) array. 24. Cheat Sheet¶
// Slice element pointer
p := &s[i]
*p = newValue
// Map of pointers (for mutation)
m := map[K]*V{}
m[k] = &V{}
m[k].Field = ...
// Pointer to slice (for header reassignment)
func reset(sp *[]int) { *sp = nil }
// Refresh after append
s = append(s, 99) // always reassign
25. Self-Assessment Checklist¶
- I know slice elements are addressable
- I know map values are not addressable
- I use
map[K]*Vfor mutable values - I'm aware of stale pointers after append
- I always reassign after append
- I use defensive copy when needed
26. Summary¶
Slices: element pointers valid, but watch for stale-after-append. Maps: values not addressable; use map[K]*V for mutable values. Both types pass by value (header/handle copied) but share underlying data — mutations propagate, reassignments don't.
27. What You Can Build¶
- Mutable scoreboards (map[K]*V)
- Slice-of-pointers for polymorphism
- Functions that grow caller's slice via *[]T
- Iterators with per-element pointer
28. Further Reading¶
29. Related Topics¶
- 2.7.1 Pointers Basics
- 2.7.2 Pointers with Structs
- 2.6.7 Call by Value
- 2.3.2 Slices, 2.3.4 Maps