Skip to content

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

run command
$ 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

run command
$ 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

run command
$ 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

run command
$ 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

run command
$ 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

run command
$ 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

run command
$ go run src/fundamentals/data_types/2d-slices.go
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.

References