Back to Notes

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:

ComponentWhat it is
PointerPoints 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 now
  • cap(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

OperationCode
Create emptyvar s []int or []int{}
Create with sizemake([]int, n)
Create with size+capmake([]int, n, cap)
Append one elements = append(s, x)
Append slices = append(s, other...)
Independent copycopy(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 lastx, s = s[len(s)-1], s[:len(s)-1]
Reversefor 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)
Sortsort.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

GotchaDetail
Shared backing arraySlices of the same array share memory — use copy to get independence
Always reassign appendappend(s, x) returns a new slice — s = append(s, x)
Modifying via range valuefor _, v := range s { v = 0 } — does nothing. Use s[i] = 0
Nil vs empty distinctionFunctions that return nil and []T{} both have len=0 but nil comparison differs
Out of boundss[len(s)] panics at runtime
copy copies min(len(dst), len(src))`dst must be pre-allocated with make