Advanced composite types: Slices
We saw in the previous chapter how to combine some fields of different types in
a container called a
struct to create a new type that we can use in our
functions and programs. We also saw how to gather n objects of the same type
into a single array that we can pass as a single value to a function.
This is fine, but do you remember :ref:`the note we wrote about arrays<arrays-note>`? Yes, that one about the fact that they don't grow or shrink, that an array of 10 elements of a given type is totally different of an array of, say, 9 elements of the same type.
This kind of inflexibility about arrays leads us to this chapter's subject.
What is a slice?
You'd love to have arrays without specifying sizes, right? Well, that is possible in a manner of speaking, we call 'dynamic arrays' in Go slices.
A slice is not actually a 'dynamic array' but rather a reference (think pointer) to a sub-array of a given array (anywhere from empty to all of it). You can declare a slice just like you declare an array, omitting the size.
As we see in the above snippet, we can declare a slice literal just like an array literal, except that we leave out the size number.
We can create slices by slicing an array or even an existing slice.
That is taking a portion (a 'slice') of it, using this syntax
Our created slice will contain elements of the array that start at index i and
end before j. (
array[j] not included in the slice)
- When slicing, first index defaults to 0, thus
ar[:n]means the same as
- Second index defaults to len(array/slice), thus
ar[n:]means the same as
- To create a slice from an entire array, we use
ar[:]which means the same as
ar[0:len(ar)]due to the defaults mentioned above.
Ok, let's go over some more examples to make this even clearer. Read slowly and pause to think about each line so that you fully grasp the concepts.
Does it make more sense now? Fine!
Is that all there is? What's so interesting about slices?
Lets show you more examples below for the yummy
sliced fruity goodness.
Slices and functions
Say we need to find the biggest value in an array of 10 elements. Good, we know
how to do it. Now, imagine that our program has the task of finding the biggest
value in many arrays of different sizes! Aha! See? One function is not enough for
that, because, remember, the types:
[m]int are different!
They can not be used as an input of a single function.
Slices fix this quite easily! In fact, we need only write a function that accepts a slice of integers as an input parameter, and create slices of arrays.
Let's see the code.
You see? Using a slice as the input parameter of our function made it reusable and we didn't had to rewrite the same function for arrays of different sizes. So remember this: whenever you think of writing a function that takes an array as its input, think again, and use a slice instead.
Let's see other advantages of slices.
Slices are references
We stated earlier that slices are references to some underlying array (or another slice). What exactly does that mean? It means that slicing an array or another slice doesn't simply copy some of the array's elements into the new slice, it instead make the new slice point to the element of this sliced array similar to the way pointers operate.
In other words: changing an element's value in a slice will actually change the value of the element in the underlying array to which the slice points. This changed value will be visible in all slices and slices of slices containing the array element.
Let's see an example:
Let's talk about the
PrintByteSlice function a little bit. If you analyze
its code, you'll see that it loops until before
len(slice)-1 to print the
slice[i] followed by a comma. At the end of the loop, it prints
slice[len(slice)-1] (the last item) with a closing bracket instead of
PrintByteSlice works with slices of bytes, of any size, and can be used to
print arrays content also, by using a slice that contains all of its elements as
an input parameter. This proves the utility of the advice before: always think
of slices when you're about to write functions for arrays.
Until now, we've learned that slices are truly useful as input parameters for functions which expect blocks of a variable size of consecutive elements of the same type. We have learned also that slices are references to portions of an array or another slice.
That is not all. Here's an awesome feature of slices.
Slices are resizable
One of our concerns with arrays is that they're inflexible. Once you declare an array, its size can't change. It won't shrink nor grow. How unfortunate!
Slices can grow or shrink.
How so? Conceptually, a slice is a
struct of three fields:
- A pointer to the first element of the array where the slice begins.
- A length which is an int representing the total number of elements in the slice.
- A capacity which is an int representing the number of available elements.
Schematically, since you love my diagrams.
The picture above is a representation of a slice that starts from the 5th element of the array. It contains exactly 4 elements (its length) and can contain up to 6 elements on the same underlying array (its capacity) starting from the first element of the slice.
Simply said: Slicing does not make a copy of the elements, it just creates a new structure holding a different pointer, length, and capacity.
To make things even easier, Go comes with a built-in function called make, which
has the signature:
func make(T, len, cap) T.
T stands for the element type of the slice to be created. The make
function takes a type, a length, and an optional capacity.
When called, make allocates an array and returns a slice that refers to that
The optional capacity parameter, when omitted, defaults to the specified length.
These parameters can be inspected for a given slice, using the built-in
len(slice) which returns a slice's length, and
cap(slice) which returns a slice's capacity.
The zero value of a slice is
cap functions will both return 0 for a
We may find ourselves wondering "Why use
make instead of
new that we discussed
when we studied :ref:`pointers<function-new>`?"
Let's discuss the reason why we use
make and not
new to create a new
new(T) allocates memory to store a variable of type
returns a pointer to it. Thus
So for example if we had used
new(int) to create a slice of integers,
new will allocate the internal structure discussed above and that will
consist of a pointer, and two integers (length and capacity).
new will then return us a pointer of the type
*int -- which...
let's face it... is not what we need!
What we do need is a function which:
- Returns the actual structure not a pointer to it. i.e. returns
- Allocates the underlying array that will hold the slice's elements.
Do you see the difference?
make doesn't merely allocate enough space to
host the structure of a given slice (the pointer, the length and the capacity)
only, it allocates both the structure and the underlying array.
Back to our resizability of slices. As you may guess, shrinking a slice is really easy! All we have to do is reslice it and assign the new slice to it.
Now, let's see how -- using only what we have learned until now -- we grow a slice. A very simple, straight forward way of growing a slice might be:
- make a new slice
- copy the elements of our slice to the one we created in 1
- make our slice refer to the slice created in 1
Let's discuss this program a little bit.
PrintIntSlice function is similar to the
function we saw earlier. Except that we improved it to support
Then the most important one is
GrowIntSlice which takes two input
int slice and an
add that represents the amount
of elements to add to the capacity.
makes a new slice with a
new_capacity that is equal
the original slice's capacity plus the
Then it copies elements from
new_slice in the highlighted loop
in lines 16, 17 and 18.
And finally, it returns the
new_slice as an output.
The approach you may be used to or that first comes to mind is often
implemented as a built-in Go function provided for you already.
This is most definitely one such example. There is a built-in Go function
copy which takes two arguments: source and destination, and copies
elements from the source to the destination and returns the number of elements
The signature of
func copy(dst, src T) int
So we can replace the lines 16, 17, 18 with this very simple one:
You know what? Let's write another example and use the
copy function in it.
Appending to a slice is a very common operation and as such Go has a built-in
append that we will see in details very soon.
Phew, that was a long chapter, eh? We'll stop here for now, go out, have a nice day, and see you tomorrow for another interesting composite type. If you are completely enthralled thus far by this exquisite book then do yourself a favor before continuing and go get a tea or coffee and take a nice 10 minute break.