Slice
Default Value
The zero value for a slice is nil
.
In Go, a slice is a more versatile and dynamic alternative to arrays. Slices are like views into an underlying array, allowing you to work with a portion of an array without specifying a fixed size
. Slices are reference types, that hold references to an underlying array, and if you assign one slice to another, both refer to the same array
. Each element in a slice is assigned to consecutive memory locations
, which makes it quick to read or write these values.
Slices are reference type
like pointers and maps.
Tip
Using [...]
makes an array. Using []
makes a slice.
Simple Use Cases
$ go run src/fundamentals/data_types/slices.go
Value in index 2: 30
All values in the slice: [10 20 30 40 50 60]
Value stored in 0 index is 10
Value stored in 1 index is 20
Value stored in 2 index is 30
Value stored in 3 index is 40
Value stored in 4 index is 50
Value stored in 5 index is 60
It is possible to change index value in array in both arrays: [10 600 30 40 50 60] [10 600 30 40 50 60]
See that mySlice didn't change mySlice: [10 999 30 40 50 60] [10 600 30 40 50 60]
package main
import "fmt"
func main() {
mySlice := []int{10, 20, 30, 40, 50, 60}
fmt.Println("Value in index 2: ", mySlice[2])
fmt.Println("All values in the slice: ", mySlice)
fmt.Println()
for i, value := range mySlice {
fmt.Printf("Value stored in %d index is %d\n", i, value)
}
fmt.Println()
mySlice2 := mySlice
mySlice2[1] = 600
fmt.Println("It's possible to change index value in array in both arrays: ", mySlice2, mySlice)
mySlice3 := make([]int, len(mySlice))
copy(mySlice3, mySlice)
mySlice3[0] = 999
fmt.Println("See that mySlice didn't change mySlice: ", mySlice3, mySlice)
}
Using make
The make function allows you to create an empty slice that already has a length or capacity specified
. When creating a slice using make
, all elements are initialized to the default value of the slice's type.
One common beginner mistake is to try to populate those initial elements using append. Is this case, the value passed to append is placed at the end of the slice.
When capacity is lower than length
You can also specify an initial capacity with make. However, never specify a capacity that’s less than the length
! It is a compile-time error to do so with a constant or numeric literal. If you use a variable to specify a capacity that’s smaller than the length, your program will panic at runtime
.
How to use make with slice
$ go run src/fundamentals/data_types/slice_make.go
Notice that the value 10 is placed in the end: [0 20 0 0 0 10]
Creating slice is len 0 but with cap--->
Notice that now value 10 is placed in the begin: [10] 1 5
[10 1 2 3 4]
package main
import "fmt"
func main() {
wrongWay := make([]int, 5)
wrongWay = append(wrongWay, 10)
wrongWay[1] = 20
fmt.Println("Notice that the value 10 is placed in the end: ", wrongWay)
fmt.Println()
fmt.Println("Creating slice is len 0 but with cap--->")
rightWay := make([]int, 0, 5)
rightWay = append(rightWay, 10)
fmt.Println("Notice that now value 10 is placed in the begin: ", rightWay, len(rightWay), cap(rightWay))
rightWay = append(rightWay, 1, 2, 3, 4)
fmt.Println(rightWay)
}
Compare Slices
A slice isn't comparable
, resulting in a compile-time error when using ==
or !=
with a slice. The only thing you can compare a slice with using ==
is nil
. However, starting from Go version 1.21
, the slices package in the standard library provides two functions for comparing slices. The slices.Equal
function accepts two slices as input parameters and returns true if both slices have the same length and all their elements are equal
. The slices.EqualFunc
function offers the flexibility to pass a custom comparison function
, allowing comparisons without the need for slice elements to be directly comparable.
Elements in slices.Equal
Notably, this function mandates that the elements of the slice must be comparable.
Operations - len, append, cap, clear
Slices are one of the supported types by the len function. The length of a slice is the number of consecutive memory locations
that have been assigned a value.
The cap
function, short for capacity, is used to determine the capacity
of slices and arrays in Go. For slices, it returns the maximum number of elements that the slice can hold without resizing the underlying array
. For arrays, it returns the length of the array.
The append
function is used to add elements to slices dynamically
. It takes a slice and one or more elements as input and returns a new slice
containing the original elements plus the additional ones. If the capacity of the original slice is sufficient to accommodate the new elements, append modifies the existing slice in place
. Otherwise, it creates a new underlying array with increased capacity and copies the elements over.
Go 1.21 added a clear
function that takes in a slice and sets all of the slice’s elements to their zero value
. The length of the slice remains unchanged.
Tip
Check out the Memory Allocation Section to understand what happens when a slice's capacity is insufficient.
Using len, append and cap
$ go run src/fundamentals/composite/slices_operations.go
Initial mySlice: [10 20 30 40 50 60]
len and cap: 6 6
Starting using append:
Appending an element --------->
mySliceAdded: [10 20 30 40 50 60 70]
len and cap: 7 12
mySlice without changes: [10 20 30 40 50 60]
len and cap: 6 6
Removing an element --------->
mySlice after remove index 3: [10 20 30 50 60]
len and cap: 5 6
mySliceAdded: [10 20 30 40 50 60 70]
Insert at index --------->
mySliceAdded after insert 1000 at index 5: [10 20 30 40 50 1000 60 70]
len and cap: 8 12
mySliceAdded after clear: [0 0 0 0 0 0 0 0] 8
package main
import "fmt"
func main() {
mySlice := []int{10, 20, 30, 40, 50, 60}
fmt.Println("Initial mySlice: ", mySlice)
fmt.Println("len and cap: ", len(mySlice), cap(mySlice))
fmt.Println()
fmt.Println("Starting using append:")
// Add an element to the end of the slice.
fmt.Println("Appending an element --------->")
mySliceAdded := append(mySlice, 70)
fmt.Println("mySliceAdded: ", mySliceAdded)
fmt.Println("len and cap: ", len(mySliceAdded), cap(mySliceAdded))
fmt.Println("mySlice without changes: ", mySlice)
fmt.Println("len and cap: ", len(mySlice), cap(mySlice))
fmt.Println()
// Remove an element from the slice by index.
fmt.Println("Removing an element --------->")
index := 3
mySlice = append(mySlice[:index], mySlice[index+1:]...) // Remove element at index
fmt.Println("mySlice after remove index 3: ", mySlice)
fmt.Println("len and cap: ", len(mySlice), cap(mySlice))
fmt.Println("mySliceAdded: ", mySliceAdded)
fmt.Println()
// Insert an element at a specific position in the slice.
fmt.Println("Insert at index --------->")
index = 5
mySliceAdded = append(
mySliceAdded[:index],
append([]int{1000}, mySliceAdded[index:]...)...) // Insert value at index
fmt.Println("mySliceAdded after insert 1000 at index 5: ", mySliceAdded)
fmt.Println("len and cap: ", len(mySliceAdded), cap(mySliceAdded))
clear(mySliceAdded)
fmt.Println("mySliceAdded after clear: ", mySliceAdded, len(mySliceAdded))
}
Memory Allocation
Remember that slices are based on arrays, so they can change in size dynamically. If the underlying array runs out of capacity, a new larger array is allocated, and the data is copied over
.
When a slice runs out of capacity, it involves allocating new memory and copying the existing data from the old memory to the new
. This process also necessitates garbage collection for the old memory. To optimize this operation, the Go runtime typically increases the slice's capacity more than just one element when it runs out of space. For slices with a capacity less than 256, the capacity is doubled
. For larger slices, the growth rate is adjusted to (current_capacity + 768)/4
, aiming for a 25% growth rate. This approach ensures that the growth rate gradually decreases as the slice's capacity increases, resulting in a more efficient memory management strategy.
This allows for more elements to be added in the future without frequent reallocations.
When a slice needs more memory...
In the case of very large slices, this can consume more memory than necessary
.
Tip
When you anticipate working with a large slice, it's a good practice to initialize the slice with a size closer to the maximum you expect to use to minimize unnecessary memory consumption
.
Tip
If you need to create a slice that’s independent of the original, see Copying a Slice.
Seeing slice changing capacity
$ go run src/fundamentals/data_types/slices_memory.go
mySlice: len=6 cap=6 [10 20 30 40 50 60]
noneValue: len=0 cap=6 []
firstFour: len=4 cap=6 [10 20 30 40]
mySlice with 70: [10 20 30 40 50 60 70]
mySlice: len=7 cap=12 [10 20 30 40 50 60 70]
firstFour: len=4 cap=6 [10 20 30 40]
noneValue: len=0 cap=6 []
package main
import "fmt"
func main() {
mySlice := []int{10, 20, 30, 40, 50, 60}
fmt.Printf("mySlice: len=%d cap=%d %v\n", len(mySlice), cap(mySlice), mySlice)
noneValue := mySlice[:0]
fmt.Printf("noneValue: len=%d cap=%d %v\n", len(noneValue), cap(noneValue), noneValue)
firstFour := mySlice[:4]
fmt.Printf("firstFour: len=%d cap=%d %v\n", len(firstFour), cap(firstFour), firstFour)
fmt.Println()
mySlice = append(mySlice, 70)
fmt.Println("mySlice with 70: ", mySlice)
fmt.Printf("mySlice: len=%d cap=%d %v\n", len(mySlice), cap(mySlice), mySlice)
fmt.Printf("firstFour: len=%d cap=%d %v\n", len(firstFour), cap(firstFour), firstFour)
fmt.Printf("noneValue: len=%d cap=%d %v\n", len(noneValue), cap(noneValue), noneValue)
}
Slicing a Slice
Same thing that slice an array.
Copying a Slice
If is necessary to create a slice that’s independent of the original, use the built-in copy
function. The copy built-in function copies elements from a source slice into a destination slice
. Copy returns the number of elements copied
.
Tip
As a special case, it also will copy bytes from a string to a slice of bytes.
The function copies as many values as it can from source to destination
, limited by whichever slice is smaller. The capacity of source and destination doesn’t matter, it’s the length that’s important.
Using Copy
$ go run src/fundamentals/data_types/slices_copy.go
Copying first 2 elements [1 2] 2
Copying last 2 elements [3 4] 2
Overlaping elements in source: [2 3 4 4] 3
Using arrays:
Copy from array [5 6]
Copying all elements from source to array: [1 2 3 4]
package main
import "fmt"
func main() {
source := []int{1, 2, 3, 4}
destination := make([]int, 2)
num := copy(destination, source)
fmt.Println("Copying first 2 elements", destination, num)
copy(destination, source[2:])
fmt.Println("Copying last 2 elements", destination, num)
num = copy(source[:3], source[1:])
fmt.Println("Overlaping elements in source: ", source, num)
fmt.Println()
fmt.Println("Using arrays: ")
source = []int{1, 2, 3, 4}
array := [4]int{5, 6, 7, 8}
destination = make([]int, 2)
copy(destination, array[:]) // create a slice from array
fmt.Println("Copy from array", destination)
copy(array[:], source)
fmt.Println("Copying all elements from source to array: ", array)
}
As Function Params
If a function takes a slice argument, changes it makes to the elements of the slice will be visible to the caller
, analogous to passing a pointer to the underlying array. Passing a slice to a function means that you are copying a pointer
.
Slices as input parameters or return values should be used carefully
. Beyond the mutability problem
, using append to change the length isn't reflected in the original variable
. A slice is implemented as a struct with three fields: two int fields for length and capacity, and a pointer to a block of memory
. Slices passed to a function can have their contents modified but the slice can't be resized.
When a slice is copied, all these fields are copied, so changing the values pointed reflects the original and the copied slice, but the length changes just for the copy
. The Go runtime prevents the original slice from seeing those new values since they are beyond the length of the original.
Slice Params
$ go run src/fundamentals/data_types/slices_params.go
Inital mySlice: [1 2 3 4]
Memory allocated for mySlice: 0xc000010018
Memory allocated for data: 0xc000010048
After call func Slice: [90 2 3 4]
Notice that append elements didn't change mySlice, just change value in index
package main
import "fmt"
func sliceParam(data []int) {
fmt.Printf("Memory allocated for data: %p\n", &data)
data[0] = 90
data = append(data, 90, 80, 70)
}
func main() {
mySlice := []int{1, 2, 3, 4}
fmt.Println("Inital mySlice: ", mySlice)
fmt.Printf("Memory allocated for mySlice: %p\n", &mySlice)
sliceParam(mySlice)
fmt.Println("After call func Slice: ", mySlice)
fmt.Println("Notice that append elements didn't change mySlice, just change value in index")
}
Two-dimensional Slices
Because slices are variable-length, it is possible to have each inner slice be a different length.
2D examples
package main
import "fmt"
type LinesOfText [][]byte
func printByLine(text LinesOfText) {
for i, line := range text {
fmt.Printf("Line %d text: %s\n", i, line)
}
fmt.Println()
}
func addLine(text LinesOfText, line string) LinesOfText {
text = append(text, []byte(line))
return text
}
func main() {
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
printByLine(text)
text[0] = []byte("CHANGED LINE")
printByLine(text)
text = addLine(text, "NEW LINE")
printByLine(text)
}
Go's Package for Slices
Package slices defines various functions useful with slices of any type. Link here.