Golang - Chapter 11

Slices

Slices are abstractions based on arrays which offer more flexibility. Slices are not arrays. Instead a slice describes a piece of an array.

Defining slices are same as defining array, except that we don't specify the element count.

a := []int{1, 2, 3, 4, 5}

The above will first create an array of length 5 and then a slice with a reference to the array.

Slicing

As mentioned, we can define slices using the high and low bounds, though they are optional. This is similar to how slices work in Python. The following example shows how we can create various slices from an array.

func main() {
    x := [5]int{1,2,3,4,5}
    
    y := x[:] // y = [1,2,3,4,5]
    y = x[2:5] // y = [3,4,5]
    y = x[2:] // y = [3,4,5]
    y = x[:3] // y = [1,2,3,4]
}

Side Notes

Slicing does not copy the slice's data. It creates a new slice value that points to the original array. This makes slice operations as efficient as manipulating array indices. Therefore, modifying the elements (not the slice itself) of a re-slice modifies the elements of the original slice:

From: https://blog.golang.org/go-slices-usage-and-internals

Passing Slices to Functions

Though slices contain a pointer to an array, they are not a pointer themselves. Instead, slices are structs which hold a pointer and a length, and later we will see the third property, capacity. Hence, when we pass slices to a function, a copy of the slice is created.

However, when we access an index of the slice within the function, we are actually accessing the index of the original array. Any modifications made to this index, will affect the original slice's values as well.

func addOne(slice []int) {
    for i := 0; i < len(slice); i++ {
        slice[i] += 1
    }
}

func main() {
    originalArray := [5]int{1, 2, 3, 4, 5}
    slice := originalArray[2:] // contains [3 4 5]
    addOne(slice)
    fmt.Println(slice)	// prints [4 5 6]
    fmt.Println(originalArray) // prints [1 2 4 5 6]
}

In the above main() function, we called addOne(), which adds 1 to each element in a slice. This call created a copy of slice and passed it to addOne(). However, when we accessed the index of the slice within addOne(), we were actually referencing the contents of originalArray.

As we incremented each element of the slice, we had actually incremented each index pointing to originalArray. Therefore, printing slice shows that the elements have indeed been incremented by one. Also, originalArray has been modified at the indexes pointed to by slice.

Side Notes

If you wanted to modify the original slice without creating a copy of it, you can create a function which accepts a pointer to the slice instead.

Length and Capacity of Slices

There are 2 attributes of a slice: length and capacity.

We can obtain the length of a slice by calling len(s) and capacity by calling cap(s), where s is the slice.

The length here refers to the number of elements the slice contains.

Capacity refers to the number of elements present in the array that the slice is pointing to (remember that slices actually are pointing to an array!). Capacity starts counting from the first element in the slice.

func main() {
    x := []int{1,2,3,4,5}
    
    x = x[:0] // empty slice, len:0, cap:5
    x = x[:4] // [1,2,3,4], len: 4, cap: 5
    x = x[2:] // [3,4], len: 2, cap: 3. Cap is 3 as we count from 3 till 5 in the underlying array.
}

Growing Slices

Now that we know the difference between length and capacity, what happens if we try to grow a slice beyond its capacity (i.e. the original array's capacity)?

func growSlice(slice []int, value int) []int {
    lengthOfSlice := len(slice)
    slice = slice[0 : lengthOfSlice + 1]
    slice[lengthOfSlice] = value
    return slice
}

func main() {
    var arr [5]int // array of size 5
    slice := arr[0:0]
    for i := 0; i < 10; i++ {
        slice = growSlice(slice, i)
        fmt.Println(slice)
    }
}

In the above example, we have growSlice() which is a function, that creates a new slice that has a new length which is one larger than before. Note that we had defined the array that the slice points to in the main() function, which has a capacity of 5.

This is the result we end up with: panic.

[0]
[0 1]
[0 1 2]
[0 1 2 3]
[0 1 2 3 4]
panic: runtime error: slice bounds out of range

When we try to grow a slice beyond the referenced array's capacity, we encounter a panic, or runtime error slice bounds out of range.

Does this mean we can never grow a slice beyond the original capacity? Of course we can! For that, we have the make function.

Using the make Function

We can also define a slice using the built-in make([]T, len, cap) []T function where T is the data type.

var s []byte
s = make([]byte, 5, 5)
// capacity defaults to the length if we omit it
s = make([]byte, 5) 

We can create a slice by slicing an array or another slice. Similar to Python, we define the part of the array to slice e.g. l := s[2:5]. We can also slice the whole array by s := x[:].

We can append to a slice. When we reach the capacity of the slice, a copy of the array is made, with a new capacity (i.e. contiguous memory) of twice the previous capacity is allocated.

s := make([]int, 0, 3)
for i := 0; i < 5; i++ {
    s = append(s, i)
    fmt.Printf("cap %v, len %v, %p\n", cap(s), len(s), s)
}

/* 
Output
cap 3, len 1, 0x1040e130
cap 3, len 2, 0x1040e130
cap 3, len 3, 0x1040e130
cap 8, len 4, 0x10432220
cap 8, len 5, 0x10432220
*/

As you can see once the capacity is met, append will return a new slice with a larger capacity. On the 4th iteration you will notice a larger capacity and a new pointer address.

References: go - cap vs len of slice in golang - Stack Overflow

Appending to Slices

We often want to add new items to an existing slice, aka appending. In Go, we have the append method for this purpose.

func main() {
    s := []int{1,2,3}
    s = append(s, 4)
    s = append(s, 5, 6, 7)
}

The append method returns a slice containing all the original values together with the newly appended values. Also, if the underlying array of the slice is too small to hold the new element, a bigger array is allocated.

Zero Values

A slice's zero value is nil. Internally as a struct, it has a length of 0, capacity of 0, and does not have an array to point to.

Two-Dimensional Slices

Slices can contain other slices. This is similar to creating 2-dimensional arrays in other languages.

We can do this by:

func main() {
    arr := [][]int{
        []int{1,2,3,4}
        []int{5,6,7,8}
        []int{9,10,11,12}
    }
}
Avatar
Harish V
Software Engineer + Tech Enthusiast

I code.

comments powered by Disqus

Related