Skip to content

Go Slice

Slices are one of the most powerful and commonly used data structures in Go. They provide a more flexible and dynamic interface to sequences of data compared to arrays.

Think of a slice as a view or a window onto an underlying array. Unlike arrays, slices have a dynamic size – they can grow and shrink.

A slice itself doesn’t store the data directly. It’s a small descriptor structure containing three fields:

Understanding this internal structure is key to understanding slice behavior, especially when slicing existing slices or using append.

There are several ways to create slices:

  1. Slice Literal: Similar to array literals but without specifying the size.
// Creates an underlying array and returns a slice referencing it
primes := []int{2, 3, 5, 7, 11, 13} // len=6, cap=6
names := []string{"Go", "Python", "Java"} // len=3, cap=3
emptySlice := []int{} // len=0, cap=0 (but not nil)
  1. Using the make function: Creates a slice, allocating an underlying array of the specified size. Elements are initialized to their zero value.
// make([]T, length, capacity)
numbers := make([]int, 5) // len=5, cap=5. Initializes with [0 0 0 0 0]
// Specify length and capacity explicitly
data := make([]byte, 3, 10) // len=3, cap=10. Initializes with [0 0 0]

Using make is useful when you know the approximate size needed, helping to avoid multiple reallocations when appending.

  1. Slicing an Existing Array or Slice: Creates a new slice that references a portion of the original array or slice.
baseArray := [6]int{0, 1, 2, 3, 4, 5}
sliceA := baseArray[1:4] // Creates a slice from index 1 (inclusive) to 4 (exclusive)
// sliceA contains [1 2 3], len=3, cap=5 (from index 1 to end of baseArray)
sliceB := sliceA[0:2] // Creates a slice from sliceA, index 0 to 2
// sliceB contains [1 2], len=2, cap=5 (still shares baseArray's capacity)
// sliceA and sliceB SHARE the same underlying array (baseArray)!
// Modifying sliceB[0] will also change sliceA[0] and baseArray[1].
  1. Declaring a Nil Slice: Declaring a slice variable without initialization results in a nil slice.
var nilSlice []int // nilSlice is nil, len=0, cap=0
fmt.Println(nilSlice == nil) // Output: true

Nil slices are useful as initial values; many standard library functions handle them gracefully (e.g., append).

The built-in len() function returns the number of elements in the slice (its length). The built-in cap() function returns the capacity of the slice (the maximum length it can reach before needing reallocation).

package main
import "fmt"
func main() {
s := make([]int, 3, 5) // length 3, capacity 5
printSliceInfo("s", s)
s = s[:cap(s)] // Extend length to capacity
printSliceInfo("s extended", s)
s = s[1:] // Reduce length from the beginning
printSliceInfo("s sliced", s)
}
func printSliceInfo(name string, x []int) {
fmt.Printf("%s: len=%d cap=%d slice=%v\n", name, len(x), cap(x), x)
}

Output:

s: len=3 cap=5 slice=[0 0 0]
s extended: len=5 cap=5 slice=[0 0 0 0 0]
s sliced: len=4 cap=4 slice=[0 0 0 0]

Slicing creates a new slice header that points to the same underlying array. Syntax: slice[low:high]

low: The starting index (inclusive). Defaults to 0 if omitted ([:high]) high: The ending index (exclusive). Defaults to len(slice) if omitted ([low:]).

The length of the new slice is high - low. The capacity is calculated from the low index of the original slice to the end of its underlying array’s capacity.

package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8} // len=9, cap=9
printSliceInfo("numbers", numbers)
// Sub-slice from index 1 (included) to 4 (excluded)
fmt.Println("numbers[1:4] ==", numbers[1:4]) // [1 2 3]
printSliceInfo("numbers[1:4]", numbers[1:4]) // len=3 cap=8
// Missing lower bound implies 0
fmt.Println("numbers[:3] ==", numbers[:3]) // [0 1 2]
printSliceInfo("numbers[:3]", numbers[:3]) // len=3 cap=9
// Missing upper bound implies len(s)
fmt.Println("numbers[4:] ==", numbers[4:]) // [4 5 6 7 8]
printSliceInfo("numbers[4:]", numbers[4:]) // len=5 cap=5
// IMPORTANT: Modifications through sub-slices affect the original!
sub := numbers[2:5] // [2 3 4], len=3, cap=7
sub[0] = 99
fmt.Println("After modifying sub[0]:")
printSliceInfo("sub", sub) // sub: len=3 cap=7 slice=[99 3 4]
printSliceInfo("numbers", numbers) // numbers: len=9 cap=9 slice=[0 1 99 3 4 5 6 7 8]
}
func printSliceInfo(name string, x []int) {
fmt.Printf("%s: len=%d cap=%d slice=%v\n", name, len(x), cap(x), x)
}

Output:

numbers: len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[1:4]: len=3 cap=8 slice=[1 2 3]
numbers[:3] == [0 1 2]
numbers[:3]: len=3 cap=9 slice=[0 1 2]
numbers[4:] == [4 5 6 7 8]
numbers[4:]: len=5 cap=5 slice=[4 5 6 7 8]
After modifying sub[0]:
sub: len=3 cap=7 slice=[99 3 4]
numbers: len=9 cap=9 slice=[0 1 99 3 4 5 6 7 8]

The built-in append function adds elements to the end of a slice. It’s crucial to understand how it works with capacity:

Crucially, you must always assign the result of append back to a slice variable, as it might return a new slice pointing to a different underlying array.

slice = append(slice, element1, element2, ...)
slice = append(slice, anotherSlice...)
package main
import "fmt"
func main() {
var numbers []int // Start with a nil slice
printSliceInfo("numbers (initial)", numbers)
// Append works on nil slices
numbers = append(numbers, 0)
printSliceInfo("numbers (appended 0)", numbers)
// Add one more element
numbers = append(numbers, 1)
printSliceInfo("numbers (appended 1)", numbers)
// Add multiple elements at once
// This might trigger a reallocation if capacity is exceeded
numbers = append(numbers, 2, 3, 4)
printSliceInfo("numbers (appended 2,3,4)", numbers)
// Append another slice using ...
moreNumbers := []int{5, 6}
numbers = append(numbers, moreNumbers...)
printSliceInfo("numbers (appended moreNumbers)", numbers)
}
func printSliceInfo(name string, x []int) {
fmt.Printf("%s: len=%d cap=%d slice=%v\n", name, len(x), cap(x), x)
}

Output (Capacity might vary slightly depending on Go version’s growth strategy):

numbers (initial): len=0 cap=0 slice=[]
numbers (appended 0): len=1 cap=1 slice=[0]
numbers (appended 1): len=2 cap=2 slice=[0 1]
numbers (appended 2,3,4): len=5 cap=6 slice=[0 1 2 3 4]
numbers (appended moreNumbers): len=7 cap=12 slice=[0 1 2 3 4 5 6]

The built-in copy function copies elements from a source slice (src) to a destination slice (dst). It returns the number of elements copied.

numCopied := copy(dst, src)

copy will copy min(len(dst), len(src)) elements. It copies element by element from source to destination and does not automatically grow the destination slice. The source and destination may overlap.

package main
import "fmt"
func main() {
source := []int{1, 2, 3, 4}
dest1 := make([]int, 2) // Destination smaller than source
num1 := copy(dest1, source)
fmt.Printf("Copied %d elements to dest1: %v\n", num1, dest1) // Output: Copied 2 elements to dest1: [1 2]
dest2 := make([]int, 5) // Destination larger than source
num2 := copy(dest2, source)
fmt.Printf("Copied %d elements to dest2: %v\n", num2, dest2) // Output: Copied 4 elements to dest2: [1 2 3 4 0]
// To create an independent copy, make a destination of the same size
independentCopy := make([]int, len(source))
copy(independentCopy, source)
independentCopy[0] = 99 // Modify the copy
fmt.Println("Source:", source) // Output: Source: [1 2 3 4]
fmt.Println("Independent Copy:", independentCopy) // Output: Independent Copy: [99 2 3 4]
}

Slices are fundamental to idiomatic Go programming, offering a balance of performance and flexibility for managing sequences of data.