Slices, Maps & Strings
Go's built-in collections look deceptively simple: you write []int, map[string]int, string and just use them. But each has a concrete layout, and it leaks out exactly where you don't expect it. A slice is not an array or a pointer but a three-word header over a separate backing array. A map is not a tree or a flat array but a bucketed hash table. A string is not text but two immutable words. Until you know this layout, the behaviour stays a set of unexplained surprises.
And those surprises are standard interview questions. A slice is passed "by value," but two copies of the header share one array — a write through one is visible through the other, hence the aliasing bugs. append past cap allocates a new array, and old slices stop seeing what was added. Writing to a nil map does not "do nothing" — it panics, even though reading from that same nil map is safe. &m[k] does not compile, and the reason is the incremental bucket evacuation during growth. And indexing a slice out of bounds does not corrupt neighbouring memory as in C — it panics. We take the topic layer by layer — from what a slice is to map rehashing and memory safety.
Topic map
- Arrays vs slices — an array is a fixed-size value copied whole; a slice is a header over a shared backing array, and that difference explains everything else.
- The slice header — what a slice is and the three machine words
{pointer, len, cap}; why a copy of the header shares elements with the original. - Slice expressions —
s[low:high]and the fulls[low:high:max], and how they set the new slice'slenandcap. - Slice growth — why
appendpastcapallocates a new backing array, what the growth factor is, and how anilslice differs from an empty one. - Slice aliasing — two slices over one backing array, the classic
appendtrap, and protection via the three-index expression. - Map basics —
make(map[K]V), thev, ok := m[k]check, the zero value for a missing key, and the deliberately randomized iteration order. - Map internals — the runtime
hmapstruct, 8-slot buckets,tophash, overflow buckets, growth, and why&m[k]does not compile. - The nil map — reading a
nilmap is safe and yields the zero value, while writing panics withassignment to entry in nil map. - Strings — a
stringis immutable UTF-8 bytes;lencounts bytes,rangeyields runes; the{pointer, len}header is 16 bytes, and converting to[]bytecopies. - Memory safety — slice indexing is bounds-checked and panics instead of corrupting neighbouring memory; how the three-index expression bounds
cap.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| Thinking a slice is passed "by reference" | Can't explain why an append inside a function is sometimes visible outside and sometimes not |
| Thinking a slice copy is independent of the original | The aliasing bug — a write through one copy corrupts the other's elements |
Confusing len and cap | A wrong mental model of when append reallocates the backing array |
Thinking append extends the backing array in place | Can't explain why old slices don't see new elements after a realloc |
Not reassigning the result of append | The new header is lost — the updated len/pointer vanish |
Confusing a nil slice with an empty slice | Both len 0, but nil equals nil and marshals to null, while []int{} marshals to [] |
Forgetting the three-index s[low:high:max] when slicing a shared buffer | An append into the subslice silently overwrites a neighbouring slice's data |
| Thinking a map is a tree or a flat array | Can't explain overflow buckets, iteration randomization, or collision behaviour |
| Relying on map iteration order | The order is deliberately randomized — collect and sort keys for determinism |
Taking the address of a map element via &m[k] | Does not compile — a map element is not addressable because of bucket evacuation |
Thinking writing to a nil map "just does nothing" | In fact panic: assignment to entry in nil map at runtime |
Confusing a string's len with its character count | For multibyte runes these differ — len counts UTF-8 bytes |
Treating string(b) and []byte(s) as free | Each conversion copies all the bytes — it's an allocation |
| Expecting a C-style buffer overflow on an out-of-bounds index | Go bounds-checks and panics index out of range rather than corrupting neighbouring memory |
Why it matters for interviews
Built-in data structures are a mandatory topic in any Go interview. They don't ask the []T or map[K]V syntax but whether you have a working model of what sits underneath and how the layout leaks into behaviour.
What interviewers usually check:
- What a slice is, which three fields make up the header, and how they change on a reslice.
- Why an array is copied whole but a slice only by header, and what aliasing bugs that leads to.
- How a slice grows on
appendpastcap, why you must reassign the result, and how anilslice differs from an empty one. - The map layout — buckets,
tophash, overflow buckets, growth, why&m[k]does not compile, and why iteration order is randomized. - Why reading from a
nilmap is safe but writing panics. - What a string header consists of, why
lencounts bytes butrangeyields runes, and why converting to[]bytecopies. - Why indexing a slice out of bounds panics rather than corrupting memory.
The typical wrong answer: "a slice is a reference type, passed by reference." That opens up the discussion that it's the three-word header that is copied by value — which is why an append that reallocated the backing array is not visible outside the function, while an append within cap is visible through the shared array.