Go Slices
Overview
A slice is Go's dynamically-sized, flexible view into an array. Unlike arrays (fixed size, part of the type), slices can grow and shrink. They are the standard way to work with ordered collections in Go — equivalent to Python's list or JavaScript's array.
Every slice has three components:
| Component | What it is |
|---|---|
| Pointer | Points to the first element in the underlying array |
Length (len) | Number of elements the slice currently has |
Capacity (cap) | Number of elements from the pointer to the end of the underlying array |
underlying array: [0, 1, 2, 3, 4, 5]
slice s: ↑———————↑
ptr len=3, cap=4
Arrays vs Slices — Key Difference
Arrays are fixed-size and rarely used directly. Slices are what you use day-to-day.
// Array — fixed, [n]T is part of the type
arr := [3]int{1, 2, 3} // type is [3]int — always exactly 3 ints
// Slice — variable size, []T
s := []int{1, 2, 3} // type is []int — can grow/shrink
Creating Slices
Slice literal
The most common way — creates both the slice and the underlying array.
s := []int{10, 20, 30}
fmt.Println(s) // [10 20 30]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 3
Slicing an array or slice — [low:high]
A slice expression creates a window into an existing array. The slice and the original share the same backing array — modifying one affects the other.
a := [5]int{1, 2, 3, 4, 5}
s1 := a[1:4] // [2 3 4] — indices 1,2,3 (high is exclusive)
s2 := a[:3] // [1 2 3] — low defaults to 0
s3 := a[2:] // [3 4 5] — high defaults to len(a)
s4 := a[:] // [1 2 3 4 5] — entire array
// Shared backing array
s1[0] = 99
fmt.Println(a) // [1 99 3 4 5] — original array changed!
make — pre-allocated slice
Use make when you know the expected size upfront. Avoids repeated reallocations.
s := make([]int, 5) // len=5, cap=5, all zeros: [0 0 0 0 0]
s := make([]int, 0, 10) // len=0, cap=10 — empty but pre-allocated for 10 elements
len and cap
len(s)— how many elements are in the slice right nowcap(s)— how many elements can fit before a new allocation is needed
s := make([]int, 3, 6)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 6
// You can extend a slice up to its capacity without reallocating
s = s[:5] // len=5, cap=6 — no new allocation
s = s[:7] // panic: beyond capacity
append — Adding Elements
append adds elements to the end of a slice and returns a new slice. If the slice has enough capacity, the elements are added in place. If not, Go allocates a new, larger array (typically doubles capacity) and copies everything.
Always assign the result back — the original slice header is not modified.
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7] — multiple at once
// Append one slice to another
a := []int{1, 2}
b := []int{3, 4, 5}
a = append(a, b...) // [1 2 3 4 5] — spread b with ...
// Build a slice iteratively
var result []int
for i := 0; i < 5; i++ {
result = append(result, i*i)
}
// result = [0 1 4 9 16]
copy — Independent Copy
copy(dst, src) copies elements from src into dst. Returns the number of elements copied (min of len(dst), len(src)). After copy, dst is independent — modifying one doesn't affect the other.
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
n := copy(dst, src)
fmt.Println(dst, n) // [1 2 3 4 5] 5
dst[0] = 99
fmt.Println(src) // [1 2 3 4 5] — unaffected
// Copy partial
small := make([]int, 3)
copy(small, src) // [1 2 3] — only 3 copied
Nil Slice vs Empty Slice
A nil slice has no backing array at all. An empty slice has a backing array but zero elements. For most operations they behave identically — but nil comparison differs.
var s1 []int // nil slice
s2 := []int{} // empty slice
s3 := make([]int, 0) // also empty
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(s3 == nil) // false
// Both are safe to use
fmt.Println(len(s1)) // 0
s1 = append(s1, 1) // works fine
for _, v := range s1 {} // safe, iterates 0 times
Convention: return
nil(not[]int{}) for empty results from functions.
Removing Elements
Go has no built-in remove — these are the standard idioms.
s := []int{1, 2, 3, 4, 5}
// Remove at index i=2 (preserve order)
i := 2
s = append(s[:i], s[i+1:]...)
// s = [1 2 4 5]
// Remove at index i (fast, but changes order — swaps with last)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// Remove first element
s = s[1:]
// Remove last element
s = s[:len(s)-1]
2D Slices
A slice of slices — each inner slice can have a different length (jagged).
// Allocate 3x3 grid
grid := make([][]int, 3)
for i := range grid {
grid[i] = make([]int, 3)
}
grid[0][0] = 1
grid[1][1] = 5
grid[2][2] = 9
// Literal syntax
board := [][]string{
{"X", "O", "X"},
{"O", "X", "O"},
{"X", "_", "O"},
}
fmt.Println(board[1][2]) // O
Slice Iteration Patterns
s := []int{1, 2, 3, 4, 5}
// Index + value
for i, v := range s { ... }
// Value only
for _, v := range s { ... }
// Index only (rare)
for i := range s { ... }
// Modify elements in place
for i := range s {
s[i] *= 2 // correct — modifies original
}
// WRONG — v is a copy, doesn't modify original
for _, v := range s {
v *= 2 // modifies local copy only
}
Slice Tricks Cheatsheet
| Operation | Code |
|---|---|
| Create empty | var s []int or []int{} |
| Create with size | make([]int, n) |
| Create with size+cap | make([]int, n, cap) |
| Append one element | s = append(s, x) |
| Append slice | s = append(s, other...) |
| Independent copy | copy(dst, src) |
Remove at index i (ordered) | s = append(s[:i], s[i+1:]...) |
Remove at index i (fast) | s[i] = s[len(s)-1]; s = s[:len(s)-1] |
| Pop last | x, s = s[len(s)-1], s[:len(s)-1] |
| Reverse | for i,j := 0,len(s)-1; i<j; i,j = i+1,j-1 { s[i],s[j] = s[j],s[i] } |
| Contains (Go 1.21+) | slices.Contains(s, x) |
| Sort | sort.Ints(s) or slices.Sort(s) (1.21+) |
How append Grows Slices
Understanding growth avoids surprises when slices share backing arrays.
s := make([]int, 0, 4) // len=0, cap=4
s = append(s, 1, 2, 3, 4) // len=4, cap=4 — fits
s = append(s, 5) // len=5, cap=8 — NEW backing array allocated!
// After reallocation, s no longer shares the old array
// Any other slice pointing to the old array is unaffected
Gotchas
| Gotcha | Detail |
|---|---|
| Shared backing array | Slices of the same array share memory — use copy to get independence |
Always reassign append | append(s, x) returns a new slice — s = append(s, x) |
Modifying via range value | for _, v := range s { v = 0 } — does nothing. Use s[i] = 0 |
| Nil vs empty distinction | Functions that return nil and []T{} both have len=0 but nil comparison differs |
| Out of bounds | s[len(s)] panics at runtime |
copy copies min(len(dst), len(src))` | dst must be pre-allocated with make |