Go Interview Prep #6: Arrays and Slices
Arrays and slices are closely related but behave very differently. Confusing them is a common source of bugs and a reliable interview topic. For a deeper runtime dive, see Slice internals.
Arrays
An array has a fixed size that is part of its type. [3]int and [5]int are different types.
var a [3]int // [0 0 0]
b := [3]int{1, 2, 3}
c := [...]int{1, 2, 3} // compiler counts the elements → [3]int
Arrays are value types: assigning or passing an array copies all elements. Mutating the copy does not affect the original.
a := [3]int{1, 2, 3}
b := a // full copy
b[0] = 99 // a[0] is still 1
Arrays are rarely used directly in application code. Most Go code uses slices. Arrays still appear as map keys (slices cannot) and in some low-level or fixed-size APIs.
Slices
A slice is a descriptor for a contiguous segment of an underlying array. Its runtime representation is a three-field header:
(pointer to array, length, capacity)
s := []int{1, 2, 3} // len=3, cap=3
s2 := s[1:3] // len=2, cap=2, shares backing array
// cap(s2) = cap(s) - 1 = 2
Because slices share backing arrays, modifying s2 modifies s:
s2[0] = 99
fmt.Println(s) // [1 99 3]
Reslicing can also extend a slice up to its capacity: s[:cap(s)] can expose elements beyond the original length (including “deleted” tail elements that are still in the array).
append
append adds elements to a slice. If there is remaining capacity, it extends the length in place (same backing array). If the slice is full, it allocates a new, larger backing array and copies the data:
s := make([]int, 0, 4)
s = append(s, 1, 2, 3) // len=3, cap=4, no alloc
s = append(s, 4, 5) // len=5, cap grows (new backing array)
Always assign the result of append back — the slice header (pointer, len, cap) is a value, not a reference. Discarding the return value is a classic bug.
When multiple slices share a backing array, an append that still has capacity can overwrite data visible through other slices. That is the main reason for the three-index form below.
copy
copy(dst, src) copies min(len(dst), len(src)) elements and returns the number copied. It never allocates.
src := []int{1, 2, 3}
dst := make([]int, 2)
n := copy(dst, src) // n=2, dst=[1,2]
copy handles overlapping slices correctly (like memmove).
The three-index slice
Go supports a three-index slice a[low:high:max] that limits the capacity of the resulting slice, preventing the sub-slice from accidentally modifying elements beyond high through append:
s := []int{1, 2, 3, 4, 5}
sub := s[1:3:3] // len=2, cap=2 (max - low = 2)
sub = append(sub, 99) // new allocation, s is not touched
Without the third index, s[1:3] would have cap=4, and append could overwrite s[3].
nil slice vs empty slice
var s []int // nil slice: s == nil, len=0, cap=0
s2 := []int{} // empty slice: s2 != nil, len=0, cap=0
s3 := make([]int, 0) // same as s2
Both behave identically for most operations (append, len, range), but encoding can differ: json.Marshal encodes a nil slice as null and an empty slice as []. Prefer nil for “no data” and empty for “known empty list” when the distinction matters to clients.
Common interview questions
Q: What is the difference between an array and a slice?
Array: fixed size (part of the type), value semantics, copied on assignment. Slice: dynamic length, header with pointer/len/cap, shares a backing array with other slices derived from it.
Q: Why must you assign the result of append?
The slice header is a struct passed by value. append may return a new pointer (new backing array) or the same pointer with an incremented length. Either way, you need the returned header.
Q: How do you delete an element from a slice?
// Delete element at index i (order preserved)
s = append(s[:i], s[i+1:]...)
// Delete element at index i (order not preserved — O(1))
s[i] = s[len(s)-1]
s = s[:len(s)-1]
Caveat: the order-preserving form still references the old tail element in the backing array until overwritten. For slices of pointers or large structs, zero the last element before shrinking if you care about GC: s[len(s)-1] = zero; s = s[:len(s)-1].
Q: How do you make a true copy of a slice?
dst := make([]int, len(src))
copy(dst, src)
// or
dst := append([]int(nil), src...)
// or (Go 1.21+)
dst := slices.Clone(src)
Q: What is the default growth factor for append?
Historically ~2× (doubling) for small slices. Since Go 1.18 the growth formula is smoother for large slices (less than double) to reduce memory waste. Treat the exact factor as an implementation detail; do not hard-code it.
Q: What does len vs cap mean?len is the number of accessible elements. cap is how far the slice can grow by reslicing or append without allocating a new array (from the current start pointer to the end of the backing array).
Quick reference
| Concept | Array | Slice |
|---|---|---|
| Size | Fixed (part of type) | Dynamic |
| Semantics | Value (copied) | Header + shared backing array |
| Zero value | All zeros | nil |
| Literal | [3]int{1,2,3} | []int{1,2,3} |
| Grow | Cannot | append (assign result!) |
| Sub-slice | a[i:j] → array window as slice | Shares backing array |
| Capacity control | N/A | s[low:high:max] |
