Grab a Slice on the Go
By: Riteek Srivastav
Tags: Programming, Programming Languages, Arrays, Tech, Golang

If you’ve used Golang, you might be familiar with the concept of arrays. In this post, we’re addressing slices. Why do slices exist when you already have arrays, you ask? We’ll talk about that, and a few more ideas, like:

What are slices and why do we need them? How do they work in Golang? What mistakes are made using slices and how to avoid them.

To understand the importance of slices, we need to talk arrays first. If you are familiar with arrays, then go ahead and jump directly to the section on slices.

Arrays

Similar to cpp, an array in Golang is a collection of the same type with continuous memory. You can access the elements directly through index.

Arrays in Golang are declared as [n]T where n is the size(length) of the array and T is the type, like int , string etc.

var a [3]int a[0] = 1 a[1] = 2 a[2] = 3 //or a := [3]int{1, 2, 3}

You can even ignore the length of the array in its declaration, and use ... instead of size ( n ).

a := [...]{1, 2, 3}

Here, the compiler will find the length for you.

Arrays in Golang are a value type, which means whenever you assign an array to a new variable, the copy of the original array is assigned to the new variable. This is also called deep copy .

a := [...]string{"Alice", "Bob", "Cop"} b := a // a copy of a is assigned to b b[0] = "Dytto" fmt.Println("a is ", a) fmt.Println("b is ", b)

The output of the above snippet will be:

a is [Alice Bob Cop] b is [Dytto Bob Cop]

The main issue an array has is that it cannot be resized (which is a very generic requirement) as the length of the array is part of its type. For example, [3]int and [5]int are two different data types.

This is where slices come in.

Slices

Simply put, slices are the wrapper over arrays. Slices do not own any data of their own, they are just a reference to the existing array.

The syntax is like:

a[start:end] creates a slice of array a from index start to end-1 . a := [5]int{1, 2, 3, 4, 5} var b []int = a[1:4] //creates a slice from a[1] to a[3] a := []int{1, 2, 3} //creates a array of 3 integers and returns the slice reference which is stored in a.

Why do we need slices?

Often, we have cases where we need to resize the length of the array (like append an array into another array). But arrays in Golang cannot be resized, hence we have slices, whose size can be dynamically changed.

Slices have length and capacity. Length is the number of elements present in the slice. Capacity is the number of elements present in the underlying array (to which the slice is referencing to), starting from the index from which the slice is created.

You can create a slice using make (like make([]T, len, cap) ) by passing length and capacity (capacity is an optional parameter and by default, it is equal to the length).

a := make([]int, 5, 10) // a is a slice of length 5 and capacity 10.

Modification of slices

As mentioned earlier, slices do not own any data on their own, they are just a reference to existing arrays. Hence, any modification done in the slice will reflect in the underlying array also. For example:

The output will be:

New elements can be added to the slice using append function. Like:

readers := []string{"Alice", "Bob", "Charlie"} fmt.Println("Readers' array :", readers, "has old length", len(readers), "and capacity", cap(readers)) readers = append(readers, "Dytto") fmt.Println("Readers' array :", readers, "has new length", len(readers), "and capacity", cap(readers))

Here, the output will be:

Readers' array : [Alice Bob Charlie] has old length 3 and capacity 3

Readers' array : [Alice Bob Charlie Dytto] has new length 4 and capacity 6

Why has the capacity changed to 6? The answer is in the next section.

Memory allocation of slices

This is the most important aspect of slices. If you do not know how the size of slices can be dynamically changed, your code might not behave as you expected. Here’s an example:

Most people would expect this output:

a: [1 2]

b: [1 2 3]

c: [1 2 4]

a: [1 2 3]

x: [1 2 3 4]

y: [1 2 3 5]

But the actual output looks like this:

a: [1 2]

b: [1 2 3]

c: [1 2 4]

a: [1 2 3]

x: [1 2 3 5]

y: [1 2 3 5]

The second one is correct. Let me explain.

When the slice length is equal to its capacity and you try to append a new element to it, then the new memory is allocated to the slice which has double capacity as compared to the earlier capacity (most of the time).

So in case 1 at line 10, a has capacity and length both equal to 1,

has capacity and length both equal to 1, In line 11, a gets allocated to a new address with capacity 2 (double the earlier one) and its length is also 2 (as two elements are present).

gets allocated to a new address with capacity 2 (double the earlier one) and its length is also 2 (as two elements are present). In line 12, a has capacity = length, hence line append(a, 3) creates a new array with capacity 4 (double the capacity of the earlier one) and returns the slice reference to b . Now b is a slice which has length = 3 and capacity =4. Here, the important point is that b is not referencing the underlying array of a , it is referencing a new array which has capacit = 4. a still has capacity and length =2.

has capacity = length, hence line creates a new array with capacity 4 (double the capacity of the earlier one) and returns the slice reference to . Now b is a slice which has length = 3 and capacity =4. Here, the important point is that is not referencing the underlying array of , it is referencing a new array which has capacit = 4. still has capacity and length =2. In line 13, the same thing is happening as line 12, since a still has the length and capacity = 2, line append(a, 4) will create a new array with length 4 (double the capacity of a as a ’s len(a) = cap(a) ) and return the slice reference to c . Now c is also a slice with len = 3 and capacity =4. And same as line 12, c is not referencing to the underlying array of a , it is referencing to a new array which has capacity = 4. a still has capacity and length = 2.

Case 1

Before line 17, a ’s length and capacity are both 2. But in line 17, a ’s capacity has been changed as line a = append(a, 3) creates a new slice with capacity 4 (double a ’s last capacity) and returns a slice reference to a itself. So after line 17, a has length = 3 (elements are 1, 2 and 3) and capacity =4.

’s length and capacity are both 2. But in line 17, ’s capacity has been changed as line creates a new slice with capacity 4 (double ’s last capacity) and returns a slice reference to itself. So after line 17, has length = 3 (elements are 1, 2 and 3) and capacity =4. Hence in line 18, x := append(a, 4) will append 4 to a (as a has capacity = 4 and currently has only 3 elements) and return the reference to x .

will append 4 to (as has capacity = 4 and currently has only 3 elements) and return the reference to . In line 19, same as line 18. y := append(a, 5) will append 5 to a (as a has capacity = 4 and currently has 3 elements) and return the reference to y.

Case 2 (This is the scene after line 19)

The interesting fact is that in case 2, a, x and y are referencing the same array. Hence, changes done by y:= append(a, 5) are being reflected in x also.

This is why the output is printing the same elements for x and y .

So, whenever you are working with slices and using append, beware of these scenarios. Also, one more important difference between array and slice is that you can compare two arrays but you cannot compare two slices in Golang. Why? 🤔 This is an exercise for the reader.