Grab a Slice on the Go
By Riteek
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:
Here, the output will be:
- 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, - 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). - In line 12,
a
has capacity = length, hence lineappend(a, 3)
creates a new array with capacity 4 (double the capacity of the earlier one) and returns the slice reference tob
. Now b is a slice which has length = 3 and capacity =4. Here, the important point is thatb
is not referencing the underlying array ofa
, it is referencing a new array which has capacit = 4.a
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, lineappend(a, 4)
will create a new array with length 4 (double the capacity ofa
asa
’slen(a)
=cap(a)
) and return the slice reference toc
. Nowc
is also a slice with len = 3 and capacity =4. And same as line 12,c
is not referencing to the underlying array ofa
, it is referencing to a new array which has capacity = 4.a
still has capacity and length = 2.
- Before line 17,
a
’s length and capacity are both 2. But in line 17,a
’s capacity has been changed as linea = append(a, 3)
creates a new slice with capacity 4 (doublea
’s last capacity) and returns a slice reference toa
itself. So after line 17,a
has length = 3 (elements are 1, 2 and 3) and capacity =4. - Hence in line 18,
x := append(a, 4)
will append 4 toa
(asa
has capacity = 4 and currently has only 3 elements) and return the reference tox
. - In line 19, same as line 18.
y := append(a, 5)
will append 5 toa
(asa
has capacity = 4 and currently has 3 elements) and return the reference to y.
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.
References:
https://golangbot.com/arrays-and-slices/
https://blog.golang.org/go-slices-usage-and-internals
Clearly, we are blessed to have smart engineers at GOJEK, and we’re also scaling fast 📈. Our 18+ products are supported by 500+ microservices, so there’s no lack of fun problems to solve. If that sounds like your kind of work environment, check out gojek.jobs, and help us shake things up in Southeast Asia 🙌