Golang Arrays and Slices

By Xavi

Arrays

Arrays in Go, as in many other programming languages, are a group of consecutive elements that are accessed through their index using the conventional subscript notation. All elements must be of the same type, for example an array of strings can only store string elements, and, if the type of an array’s elements is comparable, then the array is comparable too.

Arrays are declared as follows:

    var numbers [10]int

By default, new array elements are initialized with the zero value for their type. They can be initialized using the literal syntax:

    var numbers [10]int = [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

This expression can be shortened with the use of an ellipsis. When using an ellipsis ... instead of length, the length gets determined by the array literal’s number of initializers:

    // array of length 10
    numbers := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

Literal syntax also allows for a list of index and value pairs:

    // array of length 9
    numbers := [...]int{1: 1, 2: 2, 4: 4, 8: 8}

Using this mode of initialization, index and value pairs can be specified in any order and non-explicit indices are set to the zero value. Another consideration is array length will equal greatest index plus one.

An array’s size needs to be known at compile time. One interesting thing to note is that size of an array is part of its type, so [10]int and [12]int are different types. As the size of an array is part of its type, oftentimes they are not useful as multi-element argument for functions as the size is non variable.

By default, arrays are passed by value(copied) to functions, for the sake of performance using a pointer is preferred, especially for large ones.

 

Slices

Slices are lightweight structures built on top of arrays that allow for variable-length collections of elements with the same type. A common description is they consist of a pointer to an array, a length and a capacity.

Both declaration and initialization of slices are almost the same as their array counterparts except no size is specified. Previous examples as slices look as follows:

    var numbers []int
    var numbers []int = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    numbers :=  []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

And like arrays, follow literal syntax:

    numbers := []int{1: 1, 2: 2, 4: 4, 8: 8}

There’s also the use of the built-in function make() which can create a new slice with an anonymous underlying array:

    make([]type, length)
    make([]type, length, capacity)

If capacity is omitted like in the first line, it is equal to length and thus makes the resulting slice point to all of the underlying array elements. If capacity is specified, the resulting slice points to the first length elements of the array and leaves the rest of spaces for when the slice needs to be grown. Note capacity needs to be greater than length, otherwise Go will suggest to swap them with the message invalid argument: length and capacity swapped and prevent the building process from succeeding.

It is possible as well, to create slices based on a given array:

    // Declare and initialize array of strings
    letters := [...]string{"a", "b", "c", "d", "e"}
    ab := letters[:2]
    cde := letters[2:]

Any arbitrary number of slices can point to elements of the same array, in this case it is said they share the same underlying array. All slices pointing to elements of the same array share its contents, so when an element is changed through either one of the slices or the array, the rest see that change as well.

In the above example the slicing operator is used to create two slices pointing to elements of the same array. The slice operator is a convenient tool to express and create slices from subsegments of an array, a pointer to an array or other slices. The selection logic is inclusive of the beginning and exclusive of the end. Following the example, slice ab points to segment [letters[0]-letters[2]), or what is the same {"a", "b"}. If no index is used on a side of the colon it is substituted by either 0 if it’s the left(beginning) or the length of the array or slice if it’s the right(end). To refer to the full array or slice both indices on the sides of the colon can be omitted. The following expressions are synonyms for the whole array:

    letters[0:]
    letters[:]
    letters[:len(letters)]

Since Go 1.2, the three-index notation can be used as well. It is a mean of setting a maximum capacity manually instead of relying on the underlying array. It looks like a[i : j : k] where i ≤ j ≤ k ≤ cap(a), i represents the index of the first element pointed by the slice(inclusive), j is the last one (exclusive) and k is the last element reachable through the slice(exclusive):

    bc := letters[1:3:3]

In this particular example, slice bc has a length of 2 (j - i) and a capacity of 2 (k - i). Index i can be omitted, in which case it will be zero, it’s default value.

Slices are build up by three components: a pointer to an element of an array, a length and a capacity. The pointer points to the first element of the underlying array reachable by the slice. The first element of a slice is not necessarily the first element of the underlying array. In the example above, slice cde’s first element is "c". The length is the number of elements the slice contains, and the capacity is the number of elements from the beginning of the slice until the end of the underlying array or the third index if using three-index notation. Unlike arrays, slices are passed by reference to functions, so unless explicitly copied beforehand the called function can modify the elements of the underlying array. Continuing with the letters example, the slice ab has a length of two and a capacity of five, and the slice cde has both a length of three and a capacity of three.

When creating a slice from another one, slicing further than its length extends the resulting slice. However, trying to slice beyond its capacity causes the program to panic:

    letters := [...]string{"a", "b", "c", "d", "e"}
    ab := letters[:2]
    abcde := ab[:5] // "a", "b", "c", "d", "e"
    panics := cde[:5] // panic: runtime error: slice bounds out of range [:5] with capacity 3

Slice comparisons

Slices are not comparable. One of the reasons for this is slice elements are indirect, that is, the slice does not contain the elements but rather just points to them. This means a slice could potentially contain itself. As it would be inefficient and cumbersome to work around the cases where a slice contains itself it was decided to not allow slice comparisons. Furthermore, the use of slices as keys for a map would be problematic due to their indirect nature. Map only copies its keys shallowly and require their equality to hold during its whole lifetime, thus making slices a non-valid match.

  • The only legal slice comparison is against nil, slice == nil.
  • Slices’ zero value is nil.
  • A nil slice means it has no underlying array, and its length and capacity equal zero.
  • Only use len(slice) to check for slice emptiness.

Adding elements to slices

The main advantage of slices over plain arrays is that slices are dynamic, they can grow to fit as many elements as needed. Adding elements to a slice is done through the built-in function append().

    abcde = append(abcde, "f", "g", "h") // "a", "b", "c", "d", "e", "f", "g", "h"

append() adds one, several or even a slice of elements to the end of the slice argument, but, why assign same slice to the result of append()?

Well, that’s because in case it needs to grow the underlying array, append() returns a new pointer to a different array. Before adding the new elements to the slice argument, append() checks if there is enough space in slice’s underlying array to hold such elements. If there is, append() adds said elements to the array and returns the updated slice with the new length. In case the number of elements to be appended exceeds slice’s capacity, or what is the same, slice’s underlying array cannot hold all of them, append() allocates a new array big enough to hold all the elements, copies all slice’s elements to the new underlying array and appends the new ones. append() then returns a new slice which will point to a different underlying array, will have a total length of its previous number of elements plus the new ones and a bigger capacity. This means that in an eventual reallocation, if the result of append() gets ignored or assigned to a new variable, argument slice will remain unchanged. That’s why, as a default action, it is recommendable to always reuse the same variable in order to have a sense of continuity. Generally speaking, it cannot be known whether an append() call will trigger a reallocation of the underlying array, but the algorithm used by append() to determine slices growth can be found here.

If the new length more than doubles the current capacity, new capacity will match new length. If the old capacity is less than a threshold(in this case 256, although it used to be 1024 in the past) the new capacity will be double the current one. After that point on, it will increase its size a 25% approximately with a correcting factor to grow slower the bigger a slice is.