Let’s GO! Part 10: Reference Types 2: Slices
Overview
Slices are a very clever data type that you see in GO. They’re a concept originated in the GO language itself and you won’t find them in any other languages like they are in GO. Thus you might find them slightly hard at the first glance but they’re really easy.
Wait A Minute, What Are Them?!
Slices are basically simple views on Arrays! Think of them as a middleware class between you and your array which provides some very useful methods on your array, as well as hiding the complexity of working with arrays and providing you a very consistent and easy-to-use API to do things on arrays that weren’t possible in a simple and clean manner before.
No! They’re NOT Slower Than Arrays!
With the given explanation above, you might think that Slices might be slower than arrays since they’re providing more features than arrays and doing some magic under the hood; but that’s not true. Since the provided methods in Slices are implemented in the most efficient way possible, and since they do some magic when allocating and deallocating memory (which we’ll talk about soon), they’re even faster and more efficient than simple arrays in the most use cases!
What Magic?!
A Slice has basically 3 different parts. First part is a pointer named ptr
. A pointer to an array. Yes! You heard it right! The two other parts are two integers named len
and cap
which contain the length and the capacity of the Slice respectively. Now, when you create a Slice, basically an array gets created on the disk. A pointer to that specific array is stored inside ptr
. The length of the array is stored inside cap
and the size of the Slice itself is stored inside len
. What is happening here, is that GO tells you Ok don’t worry about anything! You want an array-like structure, you got one. You want to be able to resize it, you got it. You want all the methods to be efficient, you also got it! But how? Since a Slice is a pointer to an array, resizing it is a matter of changing the pointer and the two integers. If you initialize a Slice from an existing array or another Slice, GO will only add a pointer to that array or Slice on the disk. It doesn’t copy the data to a new place on the disk, although it will if you ask it to with the copy
function. Now if you modify one of the members of such Slice, surprise surprise, the same member of that array or the other Slice is also modified since they’re all pointing to the same place on the memory. Now since we’re only dealing with pointers and not copy pasting values over new places on the disk, it’s very efficient to work with Slices. And since GO provides built-in functions to deal with Slices, which in all cases they’re dealing with the pointers as well instead of copy pasting values, you have great power added to your array with a simple abstraction called Slice. Everybody’s happy, right?
The Difference Between len
and cap
len
is the size of the Slice. It tells you how many elements are inside this Slice. However cap
is the size of the array that this Slice points to. These two values are usually the same but sometimes they’re not. Imagine when you’re making a Slice from a range (from an index to another) from your array. That’s when len
and cap
become different. But in this case cap
won’t be the exact size of the underlying array, but the size of the underlying array starting from the element that the range starts from to the end of the array. For reading more about these details, here’s two good articles about Slices:
Code, Code, COOOOODEEEE!
Enough with the long and boring descriptions. Let’s see some code:
The output should be this:
myFirstSlice: [1 2 3]
myFirstSlice after modification: [5 2 3 10]
mySecondSlice: [5 2 3 10]
mySecondSlice after modification: [5 2 73 10]
myFirstSlice after modifying mySecondSlice: [5 2 3 10]
myThirdSlice: [5 2 3 10]
myThirdSlice after modification: [5 153 3 10]
myFirstSlice after modifying myThirdSlice: [5 153 3 10]
Length of mySecondSlice: 4
Capacity of mySecondSlice: 4
Some important notes about Slices:
- The built-in
make
function has two implementations: The first one takes 2 arguments (as you already saw in the code). The first argument is the type of your Slice, and the second one is the length of it. This way the capacity of your Slice is equal to the length you specified as the second argument. The second implementation takes 3 arguments. The first 2 arguments are as same as the first implementation, and the third one is the capacity of your Slice. This is useful when you know how much the underlying array of your Slice is going to expand throughout your application. Something a little more professional to keep an eye on the efficiency since if you expand your Slice more than it’s capacity, GO has to allocate a new space on the memory with the needed capacity and copy the data to the new location as well as changing the pointer value. Which you can say it’s somehow expensive. - Earlier we talked about ranges. If you’re familiar with Python, ranges in GO work as same as they do in Python. On both arrays and Slices, you can get a portion of them like this: Imagine you have a Slice named
mySlice
that has 6 elements inside it. You want to get the elements from index 2 to index 4 (excluding the index 4 itself). You can basically say it like this:mySlice[2:4]
. That will give you a new Slice that contains the third and the fourth elements ofmySlice
. If you don’t mention the left side of the:
, it’ll be considered from the first element of the array or Slice, and if you don’t mention the right side, it’ll be considered until the end of it. - Same as the arrays, the order of the values stored in Slices are always constant. Meaning that if you try to loop through them with a for loop, which you’ll see later on in the part about loops, you’ll get the same order of the results in each iteration. Thus it’s safe to trust their orders.
Ok well done! You’re a giga gopher now by knowing Slices!