Slices
Slices are Go's workhorse collection — dynamic views into underlying arrays with length and capacity.
Introduction
Slices are Go's workhorse collection — dynamic views into underlying arrays with length and capacity. The built-in append function grows slices, potentially reallocating. Understanding slice headers (pointer, len, cap) explains aliasing bugs that plague even experienced developers.
Slices power JSON arrays, database result sets, HTTP middleware chains, and buffer management. Mastering slice expressions (s[low:high:max]), copy, and nil vs empty slice distinction is mandatory for production Go and interviews.
This lesson includes Go 1.21+ slices package helpers (Contains, Equal, Compact) that replace hand-rolled loops in modern codebases.
The story
A Google Cloud Load Balancer health-check service maintains a dynamic list of backend endpoints. Slices grow as autoscaler events add pods and shrink when nodes drain — append amortizes allocation while [:0] resets length without freeing capacity, a trick used in high-throughput log aggregators processing millions of events per second.
Understanding slice headers (pointer, length, capacity) explains subtle bugs when sharing subslices across goroutines in a microservice mesh.
Understanding the topic
Key concepts
- Slice header: pointer to array, len, cap — s[i:j] creates sub-slice sharing backing array.
- append(s, elems...) may reallocate if cap exceeded — returns new slice header.
- nil slice vs empty slice: both len 0; nil has no backing array.
- make([]T, len, cap) preallocates; len defaults elements to zero.
- copy(dst, src) copies min(len(dst), len(src)) elements.
- Full slice expression s[low:high:max] limits cap of result slice.
flowchart LRArray[Array fixed] --> Slice[Slice view]Slice --> Append[append grows]Append --> Cap[capacity doubles]
Step-by-step explanation
- Literal: []int{1,2,3} creates slice with backing array.
- append adds elements; if len < cap, uses existing array; else doubles cap.
- Sub-slice shares array — mutation visible across slices.
- Pass slice to function — copies header only, not elements.
- Range gives index and copy of element (struct copy).
- slices.Clone (Go 1.21+) deep-copies slice elements.
Practical code example
Slice growth, sub-slicing, and safe copying:
package mainimport ("fmt""slices")func main() {s := make([]int, 0, 4)s = append(s, 1, 2, 3)fmt.Printf("s=%v len=%d cap=%d\n", s, len(s), cap(s))sub := s[1:3] // shares backing arraysub[0] = 99fmt.Println("after sub mutate:", s)cloned := slices.Clone(s)cloned[0] = 0fmt.Println("original:", s, "clone:", cloned)fmt.Println("contains 99:", slices.Contains(s, 99))}
Output
s=[1 2 3] len=3 cap=4 after sub mutate: [1 99 3] original: [1 99 3] clone: [0 99 3] contains 99: true
Line-by-line code explanation
endpoints := make([]string, 0, 32)pre-allocates capacity to reduce reallocation.endpoints = append(endpoints, podIP)grows the slice, copying when capacity is exceeded.subset := endpoints[2:5]creates a subslice sharing the underlying array.len(endpoints)is the number of elements;cap(endpoints)is underlying array size.copy(dst, src)copies elements between slices without sharing references.endpoints = endpoints[:0]resets length to zero while keeping allocated capacity for reuse.slices.Delete(endpoints, i, i+1)(Go 1.21+) removes an element without manual shifting.for i, ep := range endpointsiterates the current length, not capacity.
Key takeaway: Always consider aliasing when sub-slicing. Use slices.Clone before mutating if independence needed. Preallocate with make when size known.
Real-world use
Where you'll use this in production
- Building response DTO slices from database rows.
- Buffer pooling in high-throughput HTTP servers.
- Batch processing with sliding window sub-slices.
- Collecting errors from parallel workers into []error.
Best practices
- Preallocate: make([]T, 0, expectedSize) when count known.
- Use slices.Clone before mutating shared sub-slices.
- Prefer slices.Contains, Equal, Compact over manual loops.
- Return nil slice for 'no results' — idiomatic; json encodes as [].
- Avoid append in tight loops without prealloc — causes repeated realloc.
- Use full slice expression to prevent accidental cap extension.
Common mistakes
- Appending to slice while ranging over it — undefined behavior.
- Sub-slice aliasing causing unexpected mutations.
- Confusing nil and empty slice in JSON — both encode as [].
- Not checking append return: s = append(s, x) — append may reallocate.
- Holding pointer to slice element across append — invalid after realloc.
Advanced interview questions
Q1BeginnerSlice internals?
Q2Beginnernil vs empty slice?
Q3Intermediateappend reallocation behavior?
Q4IntermediatePrevent sub-slice aliasing bugs?
Q5AdvancedDesign API returning slice — nil or empty?
Summary
Slices are dynamic views with pointer, len, and cap. append may reallocate — always assign result: s = append(s, x). Sub-slices share backing arrays — clone before independent mutation. Preallocate with make when size is predictable. Next lesson: maps — key-value associative stores.