Go Slices 101

colinvalentini

Colin

Posted on November 5, 2022

Go Slices 101

Summary

Go provides built-in support for dynamic arrays with the slice type. A slice in Go is analogous to a list in Python, an ArrayList in Java, or an Array in JavaScript.

This post goes into the basics of slices in Go with an emphasis on common operations and their gotchas.

Internals

The slice type is a wrapper around an array. The underlying array is the storage, and the slice automatically resizes the array to fit new data. You can read more about the internals of slices here.

Being mindful of this behavior is important to avoid unwanted side effects in your applications. Some "gotchas" are noted throughout in the following sections.

Instantiating

You can create a new slice of any type using either composite literal syntax, or with the make function.

// Create a new, empty slice of integers two ways
s1 := []int{}
s2 := make([]int, 0)
Enter fullscreen mode Exit fullscreen mode

The main difference between the two options is that composite literals allow you to initialize and pre-populate a slice from known elements (literals), whereas make allows you to declare (upfront) both the length and capacity of the slice (more on this in the next section).

A slice can be created with any underlying type. See below for an example with a custom type, Foo.

type Foo struct{
    a int
    b string
}
foos := []Foo{
    {a: 1, b: "1"},
    {a: 2, b: "2"},
}
Enter fullscreen mode Exit fullscreen mode

Length vs. Capacity

Using make allows you to specify upfront what the length and (optionally) capacity of the slice should be. When the capacity argument is not provided, it is equal to the length.

The length of a slice is the number of populated elements, whereas the capacity is the total space allocated (in number of elements) for both populated and new (not yet populated) elements. As was mentioned previously, a slice will automatically resize itself if it needs to add more data (length) but does not have enough capacity to do so.

Immediately after calling make, the resulting slice is initialized with length number of elements of the natural value (or "zero value") for the slice's type. You can read more about zero values in this great post by Dave Cheney. For the purpose of this post, it's enough to know that the natural values for int and string are 0 and "" respectively.

Below we create two slices of integers. The first has a length and capacity of 3. The second has a length of 2, and capacity of 5. Immediately after calling make, both slices contain length number of integer values of 0.

s1 := make([]int, 3) // [0, 0, 0]
s2 := make([]int, 2, 5) // [0, 0]

// Appending to s2 does not create a new underlying
// array because it has  enough capacity for the new
// elements, however appending to s1 does.
s1 = append(s1, 97) // [0, 0, 0, 97]
s2 = append(s2, 98, 99, 100) // [0, 0, 98, 99, 100]
Enter fullscreen mode Exit fullscreen mode

You can find the length and capacity of a slice using the len and cap functions respectively.

s := []int{1, 2, 3}
len(s) // 3
cap(s) // 3
Enter fullscreen mode Exit fullscreen mode

Iteration

You can iterate over a slice using the range syntax (example below), or with a for loop over the length of the slice.

s := []string{"foo", "bar", "baz"}
for i, v := range s {
    // v is the element at index position i
}
Enter fullscreen mode Exit fullscreen mode

Reading and Writing

To read an element from an arbitrary index position, you can use index expressions. Below, we read the element at the second index position.

s := []string{"foo", "bar", "baz"}
s[1] // "bar"
Enter fullscreen mode Exit fullscreen mode

Similarly, to write (or assign) into an arbitrary index position we can use an assignment with an index expression.

s := []string{"foo", "bar", "baz"}
s[1] = "BINGO" // ["foo", "BINGO", "baz"]
Enter fullscreen mode Exit fullscreen mode

To read or write more than one value, you can use a simple for loop. The example below demonstrates assigning into a slice at multiple positions.

s := []string{"foo", "bar", "baz"}
for _, i := range []int{0, 2} {
    s[i] = "KAPOW" // Overwrites element at index i
}
// ["KAPOW", "bar", "KAPOW"]
Enter fullscreen mode Exit fullscreen mode

Appending

To add an element to the end of a slice, you can use the append function.

s := []int{5, 7, 1} // [5, 7, 1]
s = append(s, 9) // [5, 7, 1, 9]
Enter fullscreen mode Exit fullscreen mode

Because the append function accepts a variadic number of elements (as it's second argument), you can append multiple items at once.

s := []string{"foo"}
s = append(s, "bar", "baz")
Enter fullscreen mode Exit fullscreen mode

To extend a slice with the contents of another slice, we can unpack (or spread) another slice using the ... syntax.

s1 := []int{1, 2}
s2 := []int{3, 4, 5}
s3 := append(s1, s2...) // [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

Gotcha: The append function reuses the underlying array from the first argument when there is enough capacity to add the new elements, otherwise it creates a new one.

// Create a slice of integers with capacity for
// 4 elements but only add 3 elements.
s1 := make([]int, 3, 4)
s1[0] = 1
s1[1] = 2
s1[2] = 3

// Create s2 by appending to s1, since s1
// has enough capacity to hold a new element
// s2's underlying array is the same.
s2 := append(s1, 4)

// Appending to s1 will also append to s2
// since their underlying arrays are the same!
s1 = append(s1, 99)
Enter fullscreen mode Exit fullscreen mode

Inserting

You can use the append function to insert a new element into an arbitrary position in the slice. This is done by "stretching" out the slice at the chosen index, and then directly assigning into the slice at that position.

s := []int{1, 3, 4}

// Insert 2 into the second position
i := 1
s = append(s[:i+1], s[i:]...) // [1, 3, 3, 4]
s[i] = 2 // [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Deleting

Similar to how we can insert an element by "stretching" the slice at a given index position (and assigning), we can delete an element by "contracting" the slice at that position.

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

// Delete the element at the second position
i := 1
s = append(s[0:i], s[i+1:]...) // [1, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Copying

The built-in copy function can be used to copy the contents of one slice, the "source", into another slice, the "destination".

Gotcha: The destination slice must have at least as much length as the source slice for all elements to be copied. Otherwise, only the minimum of len(destination) and len(source) elements will be copied. You can read a great explanation of this here.

In the example below, we copy from s1 (the source) into s2 (the destination). Because s2 has 4 elements, but s1 only has 3, the result is that all 3 elements are copied into s2 but s2 still retains the rest of its data.

// Copy the contents of s1 into s2, and len(s1) < len(s2)
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6, 7}
copy(s2, s1)
// s2 is now [1, 2, 3, 7]
Enter fullscreen mode Exit fullscreen mode

In the next example, we perform a basic copy from one slice into a new slice that has been created specifically with enough space to fit the entire copy.

s1 := []int{1, 2, 3}
s2 := make([]int, len(s1)) // [0, 0, 0]
copy(s2, s1)
// s2 is now [1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

You can read more about copying as well as appending here.

Slice Expressions

Slice expressions allow you to "slice" a slice. Essentially extracting a subsequence of the elements in the slice.

The most commonly used syntax is of the form [low : high] where low is the starting index (inclusive), and high is the ending index (exclusive).

In the example below, we extract the range of elements starting from the second index position, and ending just before the fifth index position.

s := []int{1, 2, 3, 4, 5}
s2 := s[1:4] // [2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Gotcha: In the above example, s2 uses the same underlying array as s1 so updates to s2 can reflect in s1, e.g. s2[0] = 99 will update s1[1] to 99.

There is a less commonly used syntax of the form [low : high : max] where max is used to set the capacity on the resulting slice.

For example, below use the same slice expression, but the resulting slice has capacity 3 (since max - low == 4 - 1), so appending to s2 would force a new array allocation (then different from the array use to back s1).

s := []int{1, 2, 3, 4, 5}
s2 := s[1:4:4] // [2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

Nil Slices

The zero value for a slice is nil. Slices can be nil if you declare but don't initialize them, e.g. var s []int (or if you re-assign a slice variable directly to nil.

One of the nice things about slices in Go is that calling len or cap on a nil slice does not panic (they both simply return 0). A consequence of that is you don't have to guard against a nil pointer panic when performing range iteration over a possibly nil slice. However, you can't read or write into a nil slice.

var s []int // s is a nil slice
for i, v := range s {
    // Loop body is never executed since s has no elements
}
s[0] = 1 // panic
Enter fullscreen mode Exit fullscreen mode
đź’– đź’Ş đź™… đźš©
colinvalentini
Colin

Posted on November 5, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related