Understanding Backing Array— Golang

For using a list or array of values in Golang, we have two types given by the language. One is an array and the other is a slice. If you already familiar with programming languages like Java, then you might already know the difference. The array has a size value and it can’t grow whereas Slice can grow dynamically. For example to this is how an array is created and it always has a size associated with it as shown below.

var slice [1]string

slice[0] = "ABCD"
orlist := [1]string{"ABCD"}

So how do we create a slice? Golang makes our lives easier here. If you don't provide the array size then it is a slice like shown below

var slice []string

One more important difference between array and slice is that when an array is not initialized directly with values, it has all the values initialized by Go. For example, if it is an array of string then all values are filled with an empty string value, if the type is a number then all values in the array are initialized with zero. On the other hand, slices are always initialized with a value called nil.

What to use? Prefer Slice over the array as it has the advantage of auto-scaling and can be used for performance improvements by tuning the backing array.

What is the Backing array? Each slice in Golang has length and capacity properties associated with it in the name of the slice header. Length and capacity? Aren’t they similar? Yes and no depending on the context. For example

numbers := []int{10, 15,20}
fmt.Println(len(numbers)) -> 3
fmt.Println(cap(numbers)) -> 3

To be clear, the slice header contains three properties namely pointer (Memory location of first slice value), length (Length of the slice), and capacity (Capacity of the slice). Again you might ask what is the difference between length and capacity? We will get there slowly. Consider another example

numbers := []int{10, 15,20}
another := numbers[0:1]
fmt.Println(len(another)) -> 1
fmt.Println(cap(another)) -> 3

Now as you can see whenever we take a part of the slice, it refers to the same slice and it is not a new slice. This is why slices are more efficient as they just store the slice header and not the slice value. As you can see from the above example, we are slicing one element from a numbers slice hence the length is one. But capacity always refers to the total length of the slice. This is called the backing array. One caveat is that if we change the first index value of another slice it will affect the numbers slice as well. Here is one more important thing to vary of

numbers := []int{10, 15,20}
another := numbers[1:3]
fmt.Println(len(another)) -> 2
fmt.Println(cap(another)) -> 2

Interesting? You can understand why length is 2 but why capacity is 2? shouldn't it be 3? This is because another slice can’t loop back and it can only see all the elements in front of it. Hence the capacity value of 2. To further embed this idea here is one more example.

numbers := []int{10, 15,20}
another := numbers[2:3]
fmt.Println(len(another)) -> 1
fmt.Println(cap(another)) -> 1

Hope it is clear now!. Thus to conclude, slices always look back to this backing array using slice headers, and thus creating new slices out of the existing slice is very performant as they don't create a new slice and use existing reference. The only thing we need to vary of is they are mutable if not being careful.

Software Engineer