Slices

5-minute read
Table of Contents

Slices are the hip alternative to arrays in Go. They can be resized and grow in capacity, without us having to do any memory management, as we append values to them. The zero value of a slice is nil and its length is $0$:

package main

import "fmt"

func main() {
	var groceryList []string
	fmt.Printf("grocery list: %v\n", groceryList)
	fmt.Printf("is nil: %v\n", groceryList == nil)
	fmt.Printf("length: %v", len(groceryList))
}
slice-uninit.go
Copy

A slice can created with default values dependent on the zero value of the datatype used. Creating such a slice is done using the make function:

package main

import "fmt"

func main() {
	shoppingList := make([]string, 5)
	ageSlice := make([]int, 3)
	fmt.Println(shoppingList)
	fmt.Println(ageSlice)
}
make-slice.go
Copy

Notice how the shopping list slice contains a set of $5$ empty strings ("") and the age slice contains a set of $3$ zeros ($0$).

Capacity vs length

The length is the number of values currently in the slice whereas the capacity is the number of values that the slice can currently accommodate - when the capacity is exceeded, the underlying array must be reallocated in memory. We can determine the capacity of a slice by using the cap function:

package main

import "fmt"

func main() {
	mySlice := make([]int, 3)
	fmt.Println(mySlice)
	fmt.Printf("length: %d, capacity: %d\n", len(mySlice), cap(mySlice))

	// add 20 to the slice of zeros
	mySlice = append(mySlice, 20)
	fmt.Println(mySlice)
	fmt.Printf("length: %d, capacity: %d\n", len(mySlice), cap(mySlice))
}
slice-capacity.go
Copy

Note how the capacity of the array changes as we cause the length to exceed the first capacity.

The slice is just an array with extra features. The underlying array, as with any other data structure, must be allocated some memory. To expand a slice, this array must be modified to accommodate the new values.

The capacity simply determines the maximum elements that can exist in the array without reallocation becoming necessary. We can thus appreciate that the make function allows us to set a capacity in advance to avoid frequent memory reallocation when the slice needs to grow:

package main

import "fmt"

func main() {
	// slice with length of 3 and capacity of 10
	mySlice := make([]int, 3, 10)
	fmt.Println(mySlice)
	fmt.Printf("length: %d, capacity: %d\n", len(mySlice), cap(mySlice))

	// add 20 to the slice of zeros
	mySlice = append(mySlice, 20)
	fmt.Println(mySlice)
	fmt.Printf("length: %d, capacity: %d\n", len(mySlice), cap(mySlice))
}
slice-make-capacity.go
Copy

Reallocation of memory requires the computer to copy the current underlying array into a larger section of memory so frequent reallocation can slow down your program.

Shorthand notation

Because the length of slices change as we add elements to them and the capacity change is handled automatically for us whenever the length exceeds the current capacity, the shorthand notation for slices is even simpler than that for arrays:

package main

import "fmt"

func main() {
	ages := []int{14, 25, 12}
	fmt.Println(ages)

	names := []string{"John", "Jane", "Mark"}
	fmt.Println(names)
}
slice-shorthand.go
Copy

Slice operations

We have already seen in the above examples how to append values to a slice - this function accepts a slice and the new item and returns a slice containing the new item. We can also duplicate a slice by creating a new slice with the same length and using the copy function to copy the old slice’s values into the new:

package main

import "fmt"

func main() {
	friendNumbers := []int{1, 1, 2, 4}
	neighbourNumbers := make([]int, len(friendNumbers))

	fmt.Println("friend numbers: ", friendNumbers)
	fmt.Println("neighbour numbers (before copy): ", neighbourNumbers)

	copy(neighbourNumbers, friendNumbers)
	fmt.Println("neighbour numbers (after copy): ", neighbourNumbers)

}
copy-slice.go
Copy

The format is copy(destinationSlice, sourceSlice). We place the name of the slice receiving the values as the first argument of the function.

The slice of a slice

Slices have a “slice” operator which allows us to get a section of the slice as a new slice:

package main

import "fmt"

func main() {
	groceries := []string{
		"lettuce",
		"eggs",
		"butter",
		"cheese",
		"chicken",
		"beef",
	}

	// every value from index 2 to index 4, excluding index 4
	itemsBought := groceries[2:4]

	// every value from index 1 onwards
	commonItems := groceries[1:]

	// every value up to index 3, excluding index 3
	foundInSupermarket := groceries[:3]

	fmt.Println("groceries: ", groceries)
	fmt.Println("items bought: ", itemsBought)
	fmt.Println("common items: ", commonItems)
	fmt.Println("found in supermarket: ", foundInSupermarket)

	// editing the item at index 2
	groceries[2] = "cabbage"
	fmt.Println("groceries: ", groceries)
	fmt.Println("items bought: ", itemsBought)
	fmt.Println("common items: ", commonItems)
	fmt.Println("found in supermarket: ", foundInSupermarket)
}
slice-of-slice.go
Copy

Note that when we edit a value within the original slice, the slices of that slice mirror the change. This is because the slice of a slice is not a copy but rather just a reference to a sub-section of the underlying array that the original slice manages.

The slices package

We can use the Golang slices package in order to do more operations with slices:

package main

import (
	"fmt"
	"slices"
)

func main() {
	mySlice := []int{1, 2, 3}
	yourSlice := []int{1, 2, 3}
	theirSlice := []int{1, 2}
	fmt.Println("mine: ", mySlice)
	fmt.Println("yours: ", yourSlice)
	fmt.Println("theirs: ", theirSlice)

	fmt.Printf("is mine yours? %v\n", slices.Equal(mySlice, yourSlice))
	fmt.Printf("is yours theirs? %v\n", slices.Equal(yourSlice, theirSlice))
	fmt.Printf("is mine theirs? %v\n", slices.Equal(mySlice, theirSlice))
}
slices-package.go
Copy

Yes, there can be multi-dimensional slices (slices nested within slices).

Support us via BuyMeACoffee