Go Slices 101
Colin
Posted on November 5, 2022
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)
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"},
}
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]
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
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
}
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"
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"]
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"]
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]
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")
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]
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)
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]
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]
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]
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]
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]
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]
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
Posted on November 5, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.