Engineering Blog

                            

Not understanding slices length and capacity

Slice is a variable-length sequence which stores elements of a similar type, you are not allowed to store different type of elements in the same slice. It is just like an array having an index value and length, but the size of a slice is resized they are not in fixed-sized just like an array.

In general most of the Go developers to mix slice length and capacity or not understand them thoroughly. Especially, these two concepts are important for efficiently handling core operations such as slice initialization and adding elements with append, copying, or slicing. Misunderstanding uses of slices can lead to memory leakage.

In Go, a slice is backed by an array. That means the slice’s data is stored contiguously in an array data structure. A slice also handles the logic of adding an element if the backing array is full or shrinking the backing array if it’s almost empty. Internally, a slice holds a pointer to the backing array plus a length and a capacity. The length is the number of elements the slice contains. The capacity is the number of elements in the backing array. Let’s go with few example to make things clearer.

Let’s initialize a slice with a given length and capacity

s := make([]int, 3, 5)

Go provides built-in make() function to initialize a slice. make() function takes first argument as an array of data types, second argument as a length which these two arguments are mandatory even it’s take third argument as a capacity of slices which is optional argument.

slice contains 3 len and 5 cap

In this case, make creates an array of four elements (the capacity) but its length is two. A three length, five capacity slice the the length was set to 3, Go initializes only the first three elements. Also, the slice is an []int type, the default value of an int 0. The grayed elements are allocated but not yet used.

If we print this slice, we get the elements within the range of the length, [0 0 0]. If we set s[1] to 10, the second element of the slice updates without impacting its length or capacity.

Updating the slice second element i.e. s[1]=10

However, accessing an element outside the length range is forbidden, even through it’s already allocated in memory. For example, s[4] = 20 would throw the following panic:

panic: runtime error: index out of range [4] with length 3

How can we use the remaining space of the slice? By using the append built-in function:

s = append(s, 20)

This code appends to the existing slice a new element. It uses the first grayed element to store element 20, as following figure shows:

Appending element in slice

The length of the slice is updated from 3 to 4 because the slice now contains four elements. Now, what happens if we add two more elements so that the backing array isn’t large enough?

s = append(s, 30,40)
fmt.Println(s)

If we run this code, we see that the slice was able to cope with our request.

[0 10 0 20 30 40]

Because an array is a fixed-sized structure, it can store the new elements until element 30. When we want to insert element 40, the array is already full: Go internally creates another array by doubling the capacity, copying all the elements, and then inserting element 40.

Because the initial backing array is full, Go creates another array and
copies all the elements.

Note :: In Go, a slice grows by doubling its size until it contains 1,024 elements, after which it grows by 25%.

package main

import "fmt"

func main() {
	//Initialize slice
	s := make([]int, 3, 5)
	fmt.Println("Initial slice value ::", s)

	fmt.Println("---------------------------------")

	//Assign value to second index of slice
	s[2] = 10
	fmt.Println("After assigin ::", s)
	fmt.Printf("Length :: %d and Capacity :: %d \n", len(s), cap(s))
	fmt.Println("---------------------------------")

	//s[4] = 80 
	//panic: runtime error: index out of range [4] with length 3

	//Append value to slices
	s = append(s, 20)
	fmt.Println("After append :: ", s)
	fmt.Printf("Length :: %d and Capacity :: %d \n", len(s), cap(s))
	fmt.Println("---------------------------------")

	//new backing array with a new capacity
	s = append(s, 30, 40)
	fmt.Println(s)
	fmt.Printf("Length :: %d and Capacity :: %d \n", len(s), cap(s))

output of above code

In summary, the slice length is the number of available elements in the slice, whereas the slice capacity is the number of elements in the backing array. Adding an element to a full slice (length == capacity) leads to creating a new backing array with a new capacity, copying all the elements from the previous array, and updating the slice pointer to the new array.

References

  • 100 Go Mistakes and how to avoid them, Teiva Harsanyi, Manning Publications Co
Previous Post
Next Post