Go Slices
Slices are one of the most powerful and commonly used features in Go. They provide a flexible, resizable view over a sequence of elements, which makes them far more practical than fixed-size arrays for everyday programming.
What Is a Slice?
A slice is a dynamically-sized, flexible view into an underlying array. Like arrays, a slice stores elements of a single type — but unlike an array, a slice's length can grow or shrink as your program runs.
Under the hood, a slice is a small structure with three parts: a pointer to an underlying array, a length, and a capacity. Understanding this internal structure explains most of the "surprising" slice behavior you'll encounter later (such as why appending sometimes affects a related slice and sometimes doesn't).
Key benefits of slices:
- Dynamic size — can grow and shrink.
- A rich set of built-in functions (
append,copy,len,cap) for working with them. - Used far more often than arrays in real-world Go code — arrays are mostly an implementation detail slices are built on.
Three Ways to Create a Slice
1. Slice Literals
sliceName := []datatype{values}
package main
import "fmt"
func main() {
// Empty slice
numbers := []int{}
fmt.Println("numbers:", numbers)
fmt.Println("Length:", len(numbers))
fmt.Println("Capacity:", cap(numbers))
// Initialized slice
fruits := []string{"Apple", "Banana", "Cherry"}
fmt.Println("\nfruits:", fruits)
fmt.Println("Length:", len(fruits))
fmt.Println("Capacity:", cap(fruits))
}
Expected output:
numbers: []
Length: 0
Capacity: 0
fruits: [Apple Banana Cherry]
Length: 3
Capacity: 3
Understanding len() and cap():
len(slice)— the number of elements currently in the slice.cap(slice)— the number of elements the underlying array can hold before Go needs to allocate a new, larger array.
2. Slicing an Array
You can derive a slice from an array (or from another slice) using an index range:
slice := array[start:end]
The result includes index start up to, but not including, index end.
package main
import "fmt"
func main() {
grades := [6]int{85, 90, 78, 92, 88, 76}
// From index 1 up to (not including) index 4
selected := grades[1:4]
fmt.Println("Selected grades:", selected)
fmt.Println("Length:", len(selected))
fmt.Println("Capacity:", cap(selected))
}
Expected output:
Selected grades: [90 78 92]
Length: 3
Capacity: 5
The slice's capacity is measured from its start index to the end of the underlying array, not just to the slice's own end — here, the array has 6 elements, the slice starts at index 1, so capacity is 6 - 1 = 5, even though the slice's length is only 3.
3. Using make()
make() creates a slice with a predefined length and (optionally) capacity, backed by a freshly allocated array:
slice := make([]type, length, capacity)
If capacity is omitted, it defaults to equal length.
package main
import "fmt"
func main() {
// Length 3, capacity 6 — room to grow before reallocating
buffer := make([]int, 3, 6)
fmt.Println("buffer:", buffer)
fmt.Println("Length:", len(buffer))
fmt.Println("Capacity:", cap(buffer))
// Length 4, capacity defaults to 4
queue := make([]int, 4)
fmt.Println("\nqueue:", queue)
fmt.Println("Length:", len(queue))
fmt.Println("Capacity:", cap(queue))
}
Expected output:
buffer: [0 0 0]
Length: 3
Capacity: 6
queue: [0 0 0 0]
Length: 4
Capacity: 4
make() is the standard way to create a slice when you know roughly how many elements you'll need in advance, since pre-allocating capacity avoids repeated reallocation as you append elements.
When to Use Slices
Reach for a slice whenever:
- The number of elements isn't known ahead of time.
- You need to grow or shrink a collection while the program runs.
- You want to pass a collection to a function cheaply — a slice header is small and inexpensive to copy, unlike a whole array.
Accessing and Modifying Slices
Accessing Elements
Indexing works exactly like arrays — starting from 0:
package main
import "fmt"
func main() {
scores := []int{85, 90, 78, 92}
fmt.Println("First score:", scores[0])
fmt.Println("Last score:", scores[3])
}
Modifying Elements
package main
import "fmt"
func main() {
temperatures := []int{28, 30, 32}
temperatures[1] = 35 // update the second value
fmt.Println("Updated temperatures:", temperatures)
}
Expected output:
Updated temperatures: [28 35 32]
Appending Elements
append() adds one or more elements to the end of a slice and returns the resulting slice — you must always assign the result back, since append may or may not return the same underlying array:
slice = append(slice, elements...)
package main
import "fmt"
func main() {
ids := []int{101, 102, 103}
fmt.Println("Before append:", ids)
fmt.Println("Length:", len(ids), "Capacity:", cap(ids))
ids = append(ids, 104, 105)
fmt.Println("\nAfter append:", ids)
fmt.Println("Length:", len(ids), "Capacity:", cap(ids))
}
Expected output:
Before append: [101 102 103]
Length: 3 Capacity: 3
After append: [101 102 103 104 105]
Length: 5 Capacity: 6
When an append exceeds the current capacity, Go allocates a brand-new, larger underlying array (commonly doubling the previous capacity for smaller slices), copies the existing elements over, and returns a slice pointing at the new array. This is why you must always capture append's return value — the original slice variable is not updated in place.
Common mistake: forgetting that ids = append(ids, ...) is required, and instead writing just append(ids, 104, 105) without reassigning. The call still runs, but the new elements are discarded because nothing captured the returned slice.
Merging Two Slices
Use append() with the ... spread operator to append every element of one slice onto another:
package main
import "fmt"
func main() {
even := []int{2, 4, 6}
odd := []int{1, 3, 5}
combined := append(even, odd...)
fmt.Println("Combined slice:", combined)
fmt.Println("Length:", len(combined))
fmt.Println("Capacity:", cap(combined))
}
Expected output:
Combined slice: [2 4 6 1 3 5]
Length: 6
Capacity: 6
Watch out: if even had spare capacity, append(even, odd...) could overwrite elements inside even's existing underlying array rather than allocating a new one — which could silently corrupt data if another slice was still sharing that array. When in doubt about aliasing, use make() plus copy() to build an independent result, as shown below.
Changing a Slice's Length
Unlike arrays, slices can shrink and grow through re-slicing and append:
package main
import "fmt"
func main() {
data := [7]int{10, 20, 30, 40, 50, 60, 70}
slice := data[2:6]
fmt.Println("Initial:", slice, "Len:", len(slice), "Cap:", cap(slice))
slice = data[2:4]
fmt.Println("Resliced:", slice, "Len:", len(slice), "Cap:", cap(slice))
slice = append(slice, 80, 90, 100)
fmt.Println("Expanded:", slice, "Len:", len(slice), "Cap:", cap(slice))
}
Expected output:
Initial: [30 40 50 60] Len: 4 Cap: 5
Resliced: [30 40] Len: 2 Cap: 5
Expanded: [30 40 80 90 100] Len: 5 Cap: 10
Notice the "Expanded" append exceeded the original capacity of 5, so Go allocated a fresh backing array — slice is now completely independent of data.
Memory Efficiency with copy()
A slice keeps its entire underlying array alive in memory for as long as any slice still references it — even a small slice into one index of a huge array prevents the rest of that array from being garbage collected. To avoid holding onto memory you no longer need, use copy() to build a smaller, independent slice.
copy(destination, source)
- Copies elements from
sourceintodestination. - Copies as many elements as fit in the shorter of the two slices.
- Returns the number of elements actually copied.
package main
import "fmt"
func main() {
rawData := []int{5, 10, 15, 20, 25, 30, 35, 40}
fmt.Println("Original:", rawData)
fmt.Println("Length:", len(rawData), "Capacity:", cap(rawData))
subset := rawData[:3]
// A new, independent slice sized exactly to the data we need
optimized := make([]int, len(subset))
copy(optimized, subset)
fmt.Println("\nOptimized copy:", optimized)
fmt.Println("Length:", len(optimized), "Capacity:", cap(optimized))
}
Expected output:
Original: [5 10 15 20 25 30 35 40]
Length: 8 Capacity: 8
Optimized copy: [5 10 15]
Length: 3 Capacity: 3
Why this matters: once optimized exists, the original rawData array can be garbage collected as soon as nothing else references it — important in long-running programs that process large datasets in bursts.
Best Practices
- Prefer slices over arrays unless you have a specific reason to fix the size (for example, modeling something inherently fixed-length, like an RGB color's three components).
- Always reassign the result of
append()— never assume it mutates in place. - Use
make()with an estimated capacity up front when you know roughly how large a slice will grow, to avoid repeated reallocation. - Use
copy()when you need to detach a small slice from a large underlying array to free memory.