Skip to Content
Go Realm v1 is released 🎉
TopicsSlice 📌

Slices in Go

A slice is a dynamic, flexible view into an underlying array. It’s one of the most used data structures in Go.

📐 Analogy: Think of a slice like a window on a long paper roll. The window (slice) shows only a portion — but the roll (array) is still there behind it.


1. Internal Structure 🔥

Every slice is a small 3-field struct under the hood:

type slice struct { ptr *T // pointer to the first element in the underlying array len int // number of accessible elements cap int // total elements available from ptr to end of array }
s := make([]int, 3, 5) // ptr → [0, 0, 0, _, _] (underlying array of 5) // len = 3 (you can access s[0], s[1], s[2]) // cap = 5 (room for 2 more before reallocation)

2. Declaration & Initialization

var s []int // nil slice — len=0, cap=0, s==nil s := []int{} // empty slice — len=0, cap=0, s!=nil s := []int{1, 2, 3} // literal — len=3, cap=3 s := make([]int, 3) // len=3, cap=3 s := make([]int, 3, 10) // len=3, cap=10 — pre-allocated
Formnil?Use when
var s []intDefault zero value, unknown size
[]int{}Need non-nil empty slice (e.g. JSON [])
make([]T, n)Known length
make([]T, 0, n)Known capacity, will append

💡 For JSON: var s []int marshals as null; []int{} marshals as []. Use the right one!


3. len vs cap — Slicing Expressions

s := []int{10, 20, 30, 40, 50} // 0 1 2 3 4 t := s[1:3] // [20, 30]
ExpressionResultlencap
s[1:3][20, 30]24 (from index 1 to end)
s[:3][10, 20, 30]35
s[2:][30, 40, 50]33
s[:][10, 20, 30, 40, 50]55

Rule: cap = original_cap - low_index. The window starts at low and the capacity stretches to the end of the original array.


4. Capacity Growth — How append Grows 🔥

When you append beyond capacity, Go allocates a new, larger array and copies everything over. The old array is abandoned.

Observed Growth (Go 1.21, []int)

s := []int{} for i := 0; i < 17; i++ { s = append(s, i) fmt.Printf("len=%-3d cap=%d\n", len(s), cap(s)) }
Elements AddedlencapEvent
111Initial allocation
222Grew ×2
334Grew ×2
444Same capacity
558Grew ×2
6–86–88Same capacity
9916Grew ×2
10–1610–1616Same capacity
171732Grew ×2

Growth rule (simplified): ~2× for small slices. Transitions to ~1.25× growth for large slices (after ~256 elements). Exact factor is not guaranteed — never rely on a specific capacity value.

▶ Run this to observe growth live (tracks len, cap, growth ratio & reallocation)

package main import "fmt" func main() { s := []int{} prevCap := cap(s) var prevPtr *int fmt.Printf("%-5s %-5s %-8s %-12s %-12s\n", "len", "cap", "growth", "ptr", "event") for i := 0; i < 1500; i++ { s = append(s, i) var currPtr *int if len(s) > 0 { currPtr = &s[0] // address of underlying array } event := "" growth := "" if cap(s) != prevCap { if prevCap == 0 { event = "init" } else { event = "grow" growth = fmt.Sprintf("%.2f", float64(cap(s))/float64(prevCap)) } } // detect reallocation (pointer change) if prevPtr != nil && currPtr != prevPtr { event += "+realloc" } fmt.Printf("%-5d %-5d %-8s %-12p %-12s\n", len(s), cap(s), growth, currPtr, event) prevCap = cap(s) prevPtr = currPtr } }

What this proves:

  • &s[0] points to the underlying array
  • When capacity grows → pointer changes = new array allocated
  • Same pointer = same backing array, no copy happened
len cap growth ptr event 1 1 0x1400012a0 init 2 2 2.00 0x1400012c0 grow+realloc 3 4 2.00 0x140001300 grow+realloc 4 4 0x140001300 5 8 2.00 0x140001380 grow+realloc

🎯 Interview line: “You can verify slice reallocation by comparing &s[0]. When capacity grows, the pointer changes — confirming a new backing array was allocated.”

Why this matters

// ❌ Slow — O(n) amortized copies total, but triggers multiple reallocations s := []int{} for i := 0; i < 10000; i++ { s = append(s, i) } // ✅ Fast — one allocation, no reallocation s := make([]int, 0, 10000) for i := 0; i < 10000; i++ { s = append(s, i) }

5. Shared Memory — The Trap ⚠️

Sub-slices share the same underlying array. Modifying one affects the other — until append causes a reallocation.

a := []int{1, 2, 3, 4, 5} b := a[1:3] // b = [2, 3], shares a's array b[0] = 99 fmt.Println(a) // [1, 99, 3, 4, 5] — a was modified!
// ✅ Full slice expression — limit b's capacity to prevent overflow writes b := a[1:3:3] // cap of b = 3-1 = 2; append will allocate new array
// ✅ Independent copy — breaks the link b := make([]int, len(a[1:3])) copy(b, a[1:3])

6. Common Operations

s := []int{1, 2, 3, 4, 5} // Delete element at index i (order preserved) i := 2 s = append(s[:i], s[i+1:]...) // [1, 2, 4, 5] // Delete (order NOT preserved — faster) s[i] = s[len(s)-1] s = s[:len(s)-1] // Insert at index i s = append(s[:i+1], s[i:]...) s[i] = 99 // Reverse for l, r := 0, len(s)-1; l < r; l, r = l+1, r-1 { s[l], s[r] = s[r], s[l] } // Clear — two different behaviours (Go 1.21+) s = s[:0] // len=0, cap unchanged, old values still in memory (not zeroed) clear(s) // zeroes all elements, len & cap unchanged — use when data is sensitive

7. slices Package — Modern Go (1.21+)

Go 1.21 added the slices package. These replace verbose manual loops and are now idiomatic.

import "slices"
FunctionWhat it doesSince
slices.Contains(s, v)checks if v exists in s1.21
slices.Index(s, v)returns first index of v, or -11.21
slices.Reverse(s)reverses s in-place1.21
slices.Sort(s)sorts ordered types in-place1.21
slices.SortFunc(s, cmp)sorts with custom comparator1.21
slices.Equal(a, b)deep equality check1.21
slices.Delete(s, i, j)removes s[i:j] cleanly1.21
slices.Insert(s, i, v...)inserts values at index i1.21
slices.Clip(s)removes unused capacity (s[:len:len])1.21
slices.Compact(s)removes consecutive duplicates1.21
slices.Max(s) / slices.Min(s)max / min element1.21
slices.Concat(a, b, ...)concatenates multiple slices1.22
slices.Repeat(s, n)repeats slice n times1.23
s := []int{3, 1, 4, 1, 5, 9, 2, 6} slices.Sort(s) fmt.Println(s) // [1 1 2 3 4 5 6 9] fmt.Println(slices.Contains(s, 5)) // true fmt.Println(slices.Index(s, 5)) // 5 slices.Reverse(s) fmt.Println(s) // [9 6 5 4 3 2 1 1] fmt.Println(slices.Max(s)) // 9 fmt.Println(slices.Min(s)) // 1 // slices.Delete — cleaner than append trick s = slices.Delete(s, 2, 4) // removes s[2:4] // slices.Insert — cleaner than manual shift s = slices.Insert(s, 1, 99, 100) // inserts 99, 100 at index 1 // slices.Equal — replaces reflect.DeepEqual for slices fmt.Println(slices.Equal([]int{1, 2}, []int{1, 2})) // true

⚠️ Interview note: slices.Delete does not preserve order by default and may leave stale data at the tail. If you need zero-safe delete use slices.Delete + clear on the tail.


8. Array vs Slice

FeatureArraySlice
SizeFixed at compile timeDynamic
TypeValue type — full copy on assignReference type — header copy only
Comparable== works❌ only nil comparison
Pass to functionEntire array copiedOnly 3-field header copied
Common useRare (fixed buffers)Very common

9. Common Mistakes ⚠️

MistakeProblemFix
Write to nil sliceruntime panicmake or literal first
Sub-slice mutationmodifies parent unexpectedlycopy() for independence
Large array kept alive by small sub-slicememory leakcopy() into new slice
append result not assignedoriginal unchangedalways s = append(s, ...)
Assume cap after appendnon-deterministicnever rely on exact cap value
// ❌ Memory leak — 1,000,000 ints kept alive for just 10 values big := make([]int, 1_000_000) small := big[:10] // holds reference to entire backing array // ✅ Break the reference small := make([]int, 10) copy(small, big[:10])

10. Interview Cheat Sheet

Q: What are the three fields of a slice?

ptr (pointer to array), len (accessible elements), cap (total elements from ptr to end of array).

Q: What happens when append exceeds capacity?

Go allocates a new, larger array (roughly 2× for small slices), copies all elements, and returns a new slice header. The original variable is unaffected unless you reassign it.

Q: Are slices passed by value or reference?

By value — but the value is a 3-field header containing a pointer. So element modifications inside a function affect the original, but reassigning the slice (s = append(...)) does not.

Q: Difference between nil slice and empty slice?

Both have len=0. Nil slice (var s []int) has s == nil and marshals to JSON null. Empty slice ([]int{}) has s != nil and marshals to [].

Q: When does sub-slicing cause bugs?

When two slices share the same backing array — writes to one affect the other. Use copy() or the 3-index slice a[low:high:max] to prevent this.

Q: How do you pre-allocate for performance?

make([]T, 0, n) — sets capacity to n upfront so append never needs to reallocate during the filling loop.