Arrays & Slices Beginner¶
Introduction¶
Slices are the workhorse data structure of Go -- you'll use them far more than arrays. Understanding the slice header (pointer, length, capacity) and how append interacts with the underlying array is arguably the single most important data structure concept for Go interviews. Arrays are the foundation slices are built on, but are rarely used directly.
Arrays: Fixed Size, Value Type¶
Arrays have a fixed size that is part of their type. They are value types -- assignment and function arguments copy the entire array.
var a [5]int // [0, 0, 0, 0, 0] -- zero-valued
b := [3]string{"go", "is", "fun"}
c := [...]int{10, 20, 30} // compiler counts: [3]int
// Arrays are VALUE types -- this copies the entire array
d := c
d[0] = 999
fmt.Println(c[0]) // 10 -- original unchanged
// Size is part of the type
// var x [3]int = [5]int{} // compile error: [3]int ≠ [5]int
Arrays Are Rarely Used Directly
You'll almost always use slices. Arrays appear mainly as the backing store for slices, in fixed-size contexts (e.g., [32]byte for SHA-256), or when you explicitly need value semantics.
Slices: Dynamic, Reference Semantics¶
A slice is a descriptor (header) that references a segment of an underlying array. The slice header is a struct with three fields:
┌──────────────────────────────┐
│ Slice Header (24 bytes) │
│ ┌────────┬─────┬────────┐ │
│ │ ptr │ len │ cap │ │
│ │ *array │ 5 │ 8 │ │
│ └───┬────┴─────┴────────┘ │
│ │ │
│ ▼ │
│ [_][_][_][_][_][_][_][_] │
│ ← len=5 →← unused → │
│ ←──── cap=8 ────────→ │
└──────────────────────────────┘
Creating Slices¶
// From a literal
s := []int{1, 2, 3, 4, 5}
// With make(type, length, capacity)
s1 := make([]int, 5) // len=5, cap=5, zero-valued
s2 := make([]int, 0, 10) // len=0, cap=10, pre-allocated
// From an array
arr := [5]int{10, 20, 30, 40, 50}
s3 := arr[1:4] // [20, 30, 40] -- shares arr's memory
len() vs cap()¶
s := make([]int, 3, 10)
fmt.Println(len(s)) // 3 -- number of elements accessible
fmt.Println(cap(s)) // 10 -- total slots before reallocation needed
Append and Reallocation¶
append returns a new slice header. If capacity is sufficient, it extends in place; otherwise it allocates a new, larger underlying array.
s := make([]int, 0, 4)
fmt.Printf("len=%d cap=%d ptr=%p\n", len(s), cap(s), s)
s = append(s, 1, 2, 3, 4) // fits in cap=4
fmt.Printf("len=%d cap=%d ptr=%p\n", len(s), cap(s), s)
// ptr unchanged -- same backing array
s = append(s, 5) // exceeds cap, triggers reallocation
fmt.Printf("len=%d cap=%d ptr=%p\n", len(s), cap(s), s)
// ptr CHANGED -- new backing array, cap doubled to 8
Always Reassign the Result of append
append may return a header pointing to a completely different array.
Growth Strategy¶
Go's slice growth is not always 2x. As of Go 1.18+, small slices roughly double, while larger slices grow by ~25%. The exact formula is an implementation detail -- don't depend on it.
Slice Expressions¶
a := []int{0, 1, 2, 3, 4, 5, 6, 7}
// Simple slice: a[low:high]
b := a[2:5] // [2, 3, 4], len=3, cap=6
// Full slice: a[low:high:max] -- controls capacity
c := a[2:5:5] // [2, 3, 4], len=3, cap=3
// Why cap matters: c cannot see beyond index 5 of the original
// Appending to c will allocate a new array instead of overwriting a[5]
Interview Tip
The three-index slice s[low:high:max] is how you protect the original array from being overwritten by a subsequent append. This is a frequently asked interview question.
Shared Underlying Array Gotcha¶
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3] -- shares memory with original
sub[0] = 99
fmt.Println(original) // [1, 99, 3, 4, 5] -- MODIFIED!
// Appending within capacity overwrites original's data
sub = append(sub, 999)
fmt.Println(original) // [1, 99, 3, 999, 5] -- element 4 overwritten!
Safe copy pattern:
Nil Slice vs Empty Slice¶
var nilSlice []int // nil, len=0, cap=0
emptySlice := []int{} // non-nil, len=0, cap=0
makeSlice := make([]int, 0) // non-nil, len=0, cap=0
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
// Both behave identically with len, cap, append, range
fmt.Println(len(nilSlice)) // 0
nilSlice = append(nilSlice, 1) // works fine
// JSON marshaling differs!
// nil slice → "null"
// empty slice → "[]"
JSON Serialization
If your API contract requires [] instead of null for empty arrays, initialize with []int{} or make([]int, 0), not var s []int.
copy() Built-in¶
copy returns the number of elements copied, which is min(len(dst), len(src)).
src := []int{1, 2, 3, 4, 5}
// Copy into pre-allocated slice
dst := make([]int, 3)
n := copy(dst, src) // n=3, dst=[1, 2, 3]
// Copy within same slice (overlap safe)
copy(src[1:], src[2:]) // shift left: [1, 3, 4, 5, 5]
// Clone a slice (Go 1.21+ has slices.Clone)
clone := make([]int, len(src))
copy(clone, src)
Common Slice Patterns¶
// Delete element at index i (order preserved)
s = append(s[:i], s[i+1:]...)
// Delete element at index i (order NOT preserved, faster)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// Insert element at index i
s = append(s[:i], append([]T{elem}, s[i:]...)...)
// Filter in place (zero allocation)
n := 0
for _, v := range s {
if keep(v) {
s[n] = v
n++
}
}
s = s[:n]
// Pop from end (stack)
top := s[len(s)-1]
s = s[:len(s)-1]
Quick Reference¶
| Operation | Syntax | Notes |
|---|---|---|
| Declare array | var a [5]int |
Fixed size, value type |
| Declare slice | var s []int |
nil, len=0, cap=0 |
| Literal | s := []int{1, 2, 3} |
Non-nil, len=cap=3 |
| Make | make([]int, len, cap) |
Pre-allocated |
| Length | len(s) |
Elements accessible |
| Capacity | cap(s) |
Total slots |
| Append | s = append(s, v...) |
May reallocate |
| Copy | copy(dst, src) |
Returns min(len(dst), len(src)) |
| Slice | s[low:high] |
Shares memory |
| Full slice | s[low:high:max] |
Controls capacity |
| Nil check | s == nil |
Only nil, not empty |
Best Practices¶
- Pre-allocate with
makewhen you know (or can estimate) the final size -- avoids repeated reallocations - Use the three-index slice
s[lo:hi:hi]when passing sub-slices to functions that mightappend - Prefer
var s []int(nil slice) overs := []int{}unless JSON serialization requires[] - Use
copyto detach a sub-slice from its parent when you want independent data - Use
slicespackage (Go 1.21+) forslices.Clone,slices.Sort,slices.Containsetc. - Pass slices by value -- the header is only 24 bytes; the underlying array is not copied
Common Pitfalls¶
Stale slice after append
When two slices share a backing array, appending to one can silently overwrite the other's data. Use three-index slices or copy to avoid this.
Memory leak from sub-slicing
A small sub-slice can pin a large underlying array in memory:
Append in a loop with shared slices
Never pass a slice to a goroutine and continue appending in the caller -- the goroutine may see corrupted data after reallocation.
Range variable reuse
Performance Considerations¶
| Pattern | Cost | Recommendation |
|---|---|---|
append within capacity |
O(1) | Pre-allocate to avoid growth |
append exceeding capacity |
O(n) | Copy to new array |
copy |
O(n) | Cheaper than append for known sizes |
| Passing slice to function | O(1) | Only 24-byte header copied |
| Passing array to function | O(n) | Entire array copied -- use pointer or slice instead |
Pre-allocated make([]T, 0, n) |
1 alloc | Best for known-size loops |
| Delete from middle | O(n) | Shift elements; use swap-delete if order doesn't matter |
Benchmarking Tip
Pre-allocating slices is one of the most common performance wins in Go code reviews. Use make([]T, 0, n) when you know n, or estimate generously.
Interview Tips¶
Interview Tip
Be ready to draw the slice header diagram (ptr, len, cap) and explain what happens on append when capacity is exceeded. This is the #1 slice interview question.
Interview Tip
When asked "What's the difference between a nil slice and an empty slice?", say: both have len=0 and cap=0, both work with append/range/len/cap, but nil == nil is true, and JSON encoding differs (null vs []).
Interview Tip
If asked how to safely pass a sub-slice to another function, explain the three-index slice expression s[lo:hi:hi] to cap the capacity, preventing the callee's append from corrupting the caller's data.
Key Takeaways¶
- Arrays are fixed-size value types; slices are dynamic reference descriptors
- A slice header is
{pointer, length, capacity}-- 24 bytes on 64-bit appendmay return a new backing array -- always reassign the result- Sub-slices share memory with the original -- mutations are visible in both
- Use three-index slices
s[lo:hi:max]to control capacity and prevent overwrites - Pre-allocate with
make([]T, 0, n)for predictable performance - Nil slices and empty slices behave the same except for
== niland JSON encoding