Slices, Maps & Strings
Go's built-in collections from the basics up — creating and indexing slices, slice expressions, the slice header, growth and aliasing, maps with the comma-ok check and iteration order, map internals and the nil map, and strings as immutable bytes.
25 questions
JuniorTheoryVery commonHow do you create a map and check whether a key exists in Go?
How do you create a map and check whether a key exists in Go?
Create one with make(map[K]V) or a literal map[K]V{...}. Store with m[k] = v, read with m[k]. A plain read never fails: a missing key returns the value type's zero value, so you cannot tell "absent" from "present but zero". The comma-ok form v, ok := m[k] settles that — ok is false only when the key is absent. Note a nil map reads fine but panics on write, so make it first.
Common mistakes
- ✗Writing to a nil map (declared but never
maked) and hitting a panic - ✗Treating a zero-value read as proof the key is absent
- ✗Forgetting the comma-ok form and so confusing missing with zero
Follow-up questions
- →Why does reading a nil map not panic while writing to it does?
- →How do you delete a key, and what does reading it return afterwards?
JuniorTheoryVery commonWhat exactly is a Go slice and how does append grow it?
What exactly is a Go slice and how does append grow it?
A slice is a lightweight view over a backing array — a header of pointer, length, and capacity. append adds elements at the end; while capacity is free it writes in place, but once length would exceed capacity it allocates a new, larger backing array, copies the old elements over, and returns a slice pointing at it. Because that returned header may differ, you must reassign s = append(s, x).
Common mistakes
- ✗Calling
append(s, x)without reassigning, so the grown slice is lost - ✗Assuming
appendalways mutates the original backing array in place - ✗Confusing length with capacity, or thinking a slice copies its data
Follow-up questions
- →What growth factor does Go use when
appendreallocates, and why? - →After
appendreallocates, do earlier slices still see the change?
JuniorTheoryVery commonHow do fixed-size arrays differ from slices in Go, and how are they passed?
How do fixed-size arrays differ from slices in Go, and how are they passed?
An array has a fixed length that is part of its type; it is a value, so assigning or passing it copies every element. A slice is a small header referencing a backing array — passing it copies only the header, so both copies share the elements.
Common mistakes
- ✗Thinking slices are passed by reference — the header is copied by value, but it points to a shared backing array
- ✗Believing array length is not part of the type —
[3]intand[4]intare distinct, incompatible types - ✗Expecting an array passed to a function to be mutated by the callee
Follow-up questions
- →What happens when you pass a large array to a function — and how do you avoid the copy?
- →Can two slices share the same backing array, and what are the consequences?
JuniorCodeVery commonWhat does comparing a nil slice and an empty slice print?
What does comparing a nil slice and an empty slice print?
A nil slice (var a []int) has no backing array and a == nil is true; an empty slice ([]int{}) is non-nil with len 0. Both have len 0, accept append, and range cleanly. Prefer nil for "no results" — but a nil slice marshals to JSON null while an empty one marshals to [].
Common mistakes
- ✗Believing you cannot
appendto anilslice —appendallocates a backing array on first growth - ✗Assuming
[]int{}compares equal tonil— it is non-nil - ✗Overlooking that a
nilslice marshals to JSONnull, not[]
Follow-up questions
- →Why does
appendwork identically on anilslice and an empty one despite the== nildifference? - →When does the
nullvs[]JSON distinction actually break an API contract?
JuniorTheoryVery commonWhat three fields make up the slice header struct in Go?
What three fields make up the slice header struct in Go?
A slice header is three words: a pointer to the first element of the backing array, the length len (the number of accessible elements), and the capacity cap (elements from the pointer to the end of the backing array). It holds no elements itself.
Common mistakes
- ✗Confusing
lenandcap—lenis accessible elements,capis the room available before a reallocation - ✗Thinking the slice header stores the elements themselves rather than just a pointer to them
- ✗Assuming the header is a single pointer, so copying a slice is one machine word
Follow-up questions
- →What does the pointer field point to after
s = s[2:]reslices a slice? - →How are
lenandcaprelated after a slice expression likes[low:high:max]?
JuniorTheoryCommonWhat does indexing a Go string with s[i] return, and what does len(s) count?
What does indexing a Go string with s[i] return, and what does len(s) count?
s[i] returns one byte (a uint8) — the i-th raw UTF-8 byte, not a character. len(s) counts bytes too, not runes. An ASCII character is one byte, but any multi-byte UTF-8 character (say é or ы) spans several bytes, so len over-counts such text and s[i] can land mid-character. To work in characters, range over the string (which decodes runes) or convert with []rune(s) and index that.
Common mistakes
- ✗Thinking
s[i]yields a character or one-rune string rather than abyte - ✗Assuming
len(s)counts characters, so it over-counts multi-byte text - ✗Indexing into UTF-8 text and landing mid-character instead of ranging
Follow-up questions
- →Why does
for i, r := range sgive different indices thans[i]on UTF-8 text? - →What is the difference between a
byteand arunein Go?
JuniorCodeCommonWhat happens reading then writing a nil map?
What happens reading then writing a nil map?
The read m["x"] is fine and returns the zero value 0 — reading a nil map never panics. The write m["x"] = 1 panics with assignment to entry in nil map. You must initialize the map first with make(map[string]int) or a literal before writing to it.
Common mistakes
- ✗Thinking reading a
nilmap panics — only writing does - ✗Expecting the write to lazily allocate the map like
appendgrows anilslice - ✗Forgetting that a map declared with
var m map[K]Visnil, not an empty map
Follow-up questions
- →Why can you
appendto anilslice but not write to anilmap? - →What does
len(m)return on anilmap, and does ranging over it panic?
MiddleCodeCommonWhat do the cap print and the post-append x and y lines show here?
What do the cap print and the post-append x and y lines show here?
It prints 4 4, then x: [a b z d] and y: [a b z]. y := x[:2] has len 2 but inherits cap 4 to the end of x's backing array, so append(y, "z") has spare room and writes into the shared x[2], overwriting c instead of allocating.
Common mistakes
- ✗Thinking
cap(y)is 2 — a reslice inherits capacity to the end of the parent's array, so it is 4 - ✗Assuming
appendalways allocates — with spare cap it overwrites the shared element in place - ✗Forgetting that
x's length is unchanged — onlyx[2]'s value flips toz
Follow-up questions
- →How would
y := x[:2:2]changecap(y)and the result of theappend? - →Why does
x's length stay 4 even thoughx[2]was overwritten?
JuniorTheoryOccasionalIn what order does range over a map visit its entries in Go?
In what order does range over a map visit its entries in Go?
The order is randomized — Go deliberately starts each range over a map at a random bucket, so iteration order is not stable across runs or even across loops in the same run. This is by design, to stop code from accidentally depending on map internals. If you need a stable order, collect the keys into a slice and sort it.
Common mistakes
- ✗Relying on map iteration order being stable across runs
- ✗Assuming maps preserve insertion order like an ordered dictionary
- ✗Thinking maps iterate in sorted-key order
Follow-up questions
- →Why did the Go team make map iteration order random on purpose?
- →How do you print a map's entries in a deterministic, sorted order?
JuniorCodeOccasionalWhat does this slice-expression snippet print for x, y, z, d and e?
What does this slice-expression snippet print for x, y, z, d and e?
It prints x: [a b c d], y: [a b], z: [b c d], d: [b c], e: [a b c d]. The form x[low:high] yields elements from index low up to high-1, so its length is high-low; an omitted bound defaults to 0 or len(x).
Common mistakes
- ✗Treating the
highbound as inclusive —x[1:3]returns indices 1 and 2, not 1, 2, 3 - ✗Forgetting that an omitted low defaults to
0and an omitted high tolen(x) - ✗Assuming
x[:]copies the array — it returns a slice over the same backing array
Follow-up questions
- →What capacity does
y := x[:2]have, and why is it not 2? - →How would you copy the elements so the result does not share
x's backing array?
JuniorCodeOccasionalWhat does this snippet do when it indexes and tries to assign s[0]?
What does this snippet do when it indexes and tries to assign s[0]?
It does not compile. A Go string is an immutable, read-only sequence of bytes, so s[0] = ... is illegal. (s[0] reads a byte, not a string, so "R" is a type mismatch too.) To change a byte, convert: b := []byte(s); b[0] = 'R'; s = string(b).
Common mistakes
- ✗Thinking a string can be mutated in place by assigning to an index like a slice
- ✗Believing
s[0]yields a one-character string rather than abytevalue - ✗Assuming the assignment compiles and fails (or no-ops) at runtime instead of being a build error
Follow-up questions
- →Why does
fmt.Println(s[0])print a number rather than the lettert? - →After
b := []byte(s), does mutatingbaffect the original strings?
JuniorTheoryOccasionalHow is a string represented in memory in Go, and how big is a string value?
How is a string represented in memory in Go, and how big is a string value?
A string value is a two-word header — an 8-byte pointer to immutable UTF-8 bytes plus an 8-byte length — so it is 16 bytes on a 64-bit platform regardless of text length. The bytes live separately, so a struct with one string field is also 16 bytes.
Common mistakes
- ✗Assuming a string's size grows with its text length, rather than being a fixed 16-byte header
- ✗Confusing the string header with the slice header by adding a non-existent capacity field
- ✗Thinking the pointer targets a mutable buffer you can write through
Follow-up questions
- →Why can two strings share the same backing array after a slice expression like
s[1:3]? - →What does converting between
stringand[]bytecost in allocations?
MiddleTheoryOccasionalHow do a nil slice and a nil map differ when you read, append, or write?
How do a nil slice and a nil map differ when you read, append, or write?
A nil slice and a nil map both read safely — len is 0, ranging yields nothing, and a nil map read returns the value type's zero. The split is on writing. You may append to a nil slice freely: append allocates a fresh backing array, so var s []int; s = append(s, 1) just works and needs no initialization. A nil map, though, panics the moment you assign m[k] = v; you must make(map[K]V) (or use a literal) before any write.
Common mistakes
- ✗Assuming a nil map accepts writes the way a nil slice accepts
append - ✗Calling
makeon a slice beforeappendwhen it is unnecessary - ✗Forgetting that only the write side differs — reads are safe on both
Follow-up questions
- →Why can
appendwork on a nil slice but assignment cannot work on a nil map? - →When is returning a nil slice preferable to returning an empty one?
MiddleCodeOccasionalWhat do the three Println lines print after these appends?
What do the three Println lines print after these appends?
All three print their original values — [1 2 3 4 5], [2 3 4 5], [3 4]. slice1 has cap 4 and slice2 cap 3, so appending two elements overflows both. Each append allocates a fresh backing array and reassigns only the local copy of the header, so the caller's headers and arr never see it.
Common mistakes
- ✗Assuming
appendalways mutates the caller's backing array — it only writes in place when there is sparecap - ✗Forgetting that
append's reassignment touches only the local parameter copy of the header, never the caller's variable - ✗Miscomputing
slice2capacity —slice1[1:3]hascap3, not 2, so two appends still overflow
Follow-up questions
- →If
slice2hadcap4 instead of 3, whichPrintlnline would change and why? - →How would the output differ if
ModifySlicereturned the slice and the caller reassigned it?
MiddleTheoryOccasionalWhen should you choose a fixed-size array over a slice in Go?
When should you choose a fixed-size array over a slice in Go?
Choose an array when the length is a fixed compile-time constant and you want value semantics — arrays are comparable with ==, usable as map keys, and copy as a whole, avoiding a separate backing allocation. Choose a slice (the default) whenever the length varies or grows, or when you want to pass a view without copying every element.
Common mistakes
- ✗Defaulting to arrays for variable-length data instead of slices, then fighting the fixed length
- ✗Forgetting that arrays are comparable and copy by value while slices are neither and share storage
- ✗Assuming a slice is always heap-allocated, so an array is always the faster choice
Follow-up questions
- →Why can a fixed array be a map key while a slice cannot?
- →How does passing a large array by value differ from passing a slice header?
MiddleTheoryOccasionalWhat is a buffer overflow, and can it happen in ordinary Go code?
What is a buffer overflow, and can it happen in ordinary Go code?
A buffer overflow is writing past the end of an array or buffer, corrupting adjacent memory — possible in C because there are no bounds checks. In ordinary Go it cannot happen: slice and array accesses are bounds-checked, and an out-of-range index panics instead of overwriting memory. You can only bypass this with the unsafe package.
Common mistakes
- ✗Thinking Go skips bounds checks for performance the way C does
- ✗Believing
appendpast capacity overflows instead of allocating a new backing array - ✗Forgetting that
unsafeis the only escape hatch around bounds checking
Follow-up questions
- →What runtime error do you get from indexing a slice out of range, and how is it phrased?
- →Why does the compiler sometimes elide bounds checks, and how can you confirm it did?
MiddleTheoryOccasionalHow is a Go map implemented internally with buckets and hashing?
How is a Go map implemented internally with buckets and hashing?
A Go map is an hmap struct pointing at an array of buckets. Each bucket holds up to 8 key/value slots plus a link to overflow buckets. A key's hash selects the bucket; its top bits speed slot scanning. When buckets fill up, the map grows and rehashes into a larger array.
Common mistakes
- ✗Thinking a map is a tree or a flat array rather than a bucket-based hash table
- ✗Believing a hash collision overwrites or loses entries instead of chaining into overflow buckets
- ✗Assuming each bucket holds one entry rather than up to 8 slots
Follow-up questions
- →Why does Go deliberately randomize the iteration order of a map?
- →What is an overflow bucket and when does the runtime allocate one?
MiddleCodeOccasionalWhat does append(a[:1], 99) print for a and b?
What does append(a[:1], 99) print for a and b?
It prints [1 99 3] [1 99]. a[:1] has len 1 but cap 3, so append has spare capacity and writes 99 into the same backing array at index 1, overwriting a[1]. Both slices view that array. Force a copy with the full-slice expression a[:1:1] so append reallocates.
Common mistakes
- ✗Assuming
appendalways returns a fresh array — it reuses the backing array whenevercap > len - ✗Confusing
lenwithcap—a[:1]haslen 1but inheritscap 3froma - ✗Forgetting the full-slice expression
a[:1:1]caps capacity to force a reallocation
Follow-up questions
- →How does
a[:1:1]change the capacity, and why does that prevent the aliasing? - →If two
appendcalls share one base slice with sparecap, why does the second overwrite the first?
MiddleTheoryOccasionalHow do you protect a slice from sharing a backing array with another?
How do you protect a slice from sharing a backing array with another?
Make an independent copy with copy(dst, src) into a freshly allocated slice, or cap capacity with the three-index expression s[low:high:max] so the next append must reallocate instead of overwriting shared elements. Return copies, not sub-slices, from APIs so callers cannot mutate your backing array.
Common mistakes
- ✗Confusing pass-by-pointer with copying — a pointer still reaches the same backing array
- ✗Forgetting the third index —
s[low:high]keeps the parent's capacity, soappendcan still alias - ✗Returning a sub-slice from an API, letting callers mutate the internal backing array
Follow-up questions
- →Why does
copyrequire a destination of sufficient length rather than capacity? - →How does
s[low:high:max]differ froms[low:high]for the nextappend?
MiddleTheoryOccasionalHow does a slice grow when append exceeds its capacity in Go?
How does a slice grow when append exceeds its capacity in Go?
When append needs more room than cap, the runtime allocates a new, larger backing array, copies the existing elements into it, and returns a new header pointing there. Growth roughly doubles capacity for small slices and uses a smaller factor (about 1.25x) for large ones.
Common mistakes
- ✗Thinking
appendgrows the backing array in place rather than allocating a fresh one - ✗Believing growth always exactly doubles — large slices grow by a smaller factor
- ✗Assuming other slices sharing the old backing array see appended elements after a reallocation
Follow-up questions
- →Why must you always assign the result of
appendback to the slice variable? - →How does pre-sizing with
make([]T, 0, n)avoid repeated reallocations?
MiddleTheoryOccasionalWhat pitfalls do Go slices have because of the shared backing array?
What pitfalls do Go slices have because of the shared backing array?
Slices share a backing array, so three pitfalls follow. Writing through one slice mutates others where ranges overlap. append may or may not touch the original, depending on spare capacity. And a small sub-slice keeps the whole large array alive, blocking GC. Mitigate with copy or s[low:high:max].
Common mistakes
- ✗Believing a sub-slice is an independent copy — it aliases the parent's backing array
- ✗Assuming
appendalways reallocates — within sparecapit overwrites shared elements - ✗Overlooking memory retention — a tiny sub-slice can pin a huge array in memory
Follow-up questions
- →How does a three-index slice
s[low:high:max]prevent the aliasing on the nextappend? - →Why can a small sub-slice of a large array cause a memory leak, and how do you fix it?
MiddleCodeOccasionalWhat does this loop print after reassigning the slice during range?
What does this loop print after reassigning the slice during range?
It prints a b c d. range evaluates its operand once at the start and iterates over a copy of the slice header (pointer, len, cap). Reassigning lst to a brand-new slice only rebinds the variable — the loop keeps iterating the original backing array. If instead you mutated an element of the original backing array, e.g. lst[3] = "z", the loop would see it and print a b c z.
Common mistakes
- ✗Thinking
rangere-reads the slice variable each iteration - ✗Believing reassigning the variable changes the backing array being iterated
- ✗Expecting a panic instead of stable iteration over the original array
Follow-up questions
- →Why does mutating
lst[3]show up in the loop but reassigninglstdoes not? - →What three fields are copied when
rangesnapshots the slice header?
SeniorTheoryRareHow does a Go map lookup scan a bucket — tophash bytes and overflow chains?
How does a Go map lookup scan a bucket — tophash bytes and overflow chains?
A lookup hashes the key, selects a bucket, then compares the hash's top byte (tophash) against each of the bucket's 8 slots — a 1-byte filter that skips full key comparison on a miss. Only a tophash match triggers a full key compare. If all 8 slots miss, it follows the bucket's overflow pointer and repeats down the chain.
Common mistakes
- ✗Thinking the runtime does a full key comparison in every slot rather than filtering on the 1-byte
tophashfirst - ✗Believing a hash collision overwrites an entry instead of chaining into an overflow bucket
- ✗Assuming a
tophashmatch means the keys are equal and skipping the full key comparison
Follow-up questions
- →When does the runtime allocate an overflow bucket instead of growing the whole map?
- →How does the
tophashbyte also encode empty-slot and evacuation markers?
SeniorTheoryRareWhy can't you take the address of a map element, and what is bucket evacuation?
Why can't you take the address of a map element, and what is bucket evacuation?
A map element is not addressable because growth relocates entries: when the map exceeds its load factor it allocates a larger bucket array and evacuates entries incrementally — each bucket is rehashed and copied during later writes. A stored &m[k] would dangle after a move, so it does not compile.
Common mistakes
- ✗Thinking
&m[k]fails only for unexported types rather than for every map element - ✗Believing bucket evacuation happens all at once during the triggering insert
- ✗Assuming a pointer obtained from a map would stay valid across later inserts
Follow-up questions
- →How can a struct-valued map be mutated in place given elements are not addressable?
- →Why does the runtime spread evacuation across writes instead of doing it eagerly?