Slices in Go
Table of Contents
99 times out of 100 you will use a slice instead of an array when working with ordered lists.
All the content from our Boot.dev courses are available for free here on the blog. This one is the "Slices" chapter of Learn Go. If you want to try the far more immersive version of the course, do check it out!
Arrays and Slices in Go
Arrays are fixed-size groups of variables of the same type. For example, [4]string is an array of 4 values of type string.
To declare an array of 10 integers:
var myInts [10]int
or to declare an initialized literal:
primes := [6]int{2, 3, 5, 7, 11, 13}
Once you make an array like [10]int you can't add an 11th element.
A slice is a dynamically-sized, flexible view of the elements of an array.
The zero value of slice is nil.
Non-nil slices always have an underlying array, though it isn't always specified explicitly. To explicitly create a slice on top of an array we can do:
primes := [6]int{2, 3, 5, 7, 11, 13}
mySlice := primes[1:4]
// mySlice = {3, 5, 7}
The syntax is:
arrayname[lowIndex:highIndex]
arrayname[lowIndex:]
arrayname[:highIndex]
arrayname[:]
Where lowIndex is inclusive and highIndex is exclusive.
lowIndex, highIndex, or both can be omitted to use the entire array on that side of the colon.
Slices Hold References to an Underlying Array
Slices wrap arrays to give a more general, powerful, and convenient interface to sequences of data. Except for items with explicit dimensions such as transformation matrices, most array programming in Go is done with slices rather than simple arrays.
Slices hold references to an underlying array, and if you assign one slice to another, both refer to the same array. If a function takes a slice argument, any changes it makes to the elements of the slice will be visible to the caller, analogous to passing a pointer to the underlying array. A Read function can therefore accept a slice argument rather than a pointer and a count; the length within the slice sets an upper limit of how much data to read. Here is the signature of the Read() method of the File type in package os:
Referenced from Effective Go
func (f *File) Read(buf []byte) (n int, err error)
Creating Slices With make and Literals
Most of the time we don't need to think about the underlying array of a slice. We can create a new slice using the make function:
// func make([]T, len, cap) []T
mySlice := make([]int, 5, 10)
// the capacity argument is usually omitted and defaults to the length
mySlice := make([]int, 5)
Slices created with make will be filled with the zero value of the type.
If we want to create a slice with a specific set of values, we can use a slice literal:
mySlice := []string{"I", "love", "go"}
Notice the square brackets do not have a 3 in them. If they did, you'd have an array instead of a slice.
var and new
There are a couple of other ways to declare a slice, and they aren't interchangeable:
var varStyle []string
newStyle := new([]string)
var varStyle []string is the idiomatic way to declare an empty slice. The slice is actually nil, which means it will be null when marshalled to JSON and will succeed nil checks.
newStyle := new([]string) returns a pointer to the slice. Same as ptrStyle := &[]string{}. Only use if you want a pointer.
A slice literal like []string{} should probably only be used when the literal is going to start with values in it, as in []string{"cat", "dog"}. Otherwise prefer make().
Length
The length of a slice is simply the number of elements it contains. It is accessed using the built-in len() function:
mySlice := []string{"I", "love", "go"}
fmt.Println(len(mySlice)) // 3
Capacity
The capacity of a slice is the number of elements in the underlying array, counting from the first element in the slice. It is accessed using the built-in cap() function:
mySlice := []string{"I", "love", "go"}
fmt.Println(cap(mySlice)) // 3
Generally speaking, unless you're hyper-optimizing the memory usage of your program, you don't need to worry about the capacity of a slice because it will automatically grow as needed.
If we know the rough size of a slice before we fill it up, we can make our program faster by creating the slice with that size ahead of time so that the Go runtime doesn't need to continuously allocate new underlying arrays of larger and larger sizes. By setting the length, the slice can still be resized later, but it means we can avoid all the expensive resizing since we know what we'll need.
Indexing
A programming concept you should already be familiar with is array indexing. You can access or assign a single element of an array or slice by using its index.
mySlice := []string{"I", "love", "go"}
fmt.Println(mySlice[2]) // go
mySlice[0] = "you"
fmt.Println(mySlice) // [you love go]
How Slices Grow
The length of a slice may be changed as long as it still fits within the limits of the underlying array; just assign it to a slice of itself. The capacity of a slice, accessible by the built-in function cap, reports the maximum length the slice may assume. Here is a function to append data to a slice. If the data exceeds the capacity, the slice is reallocated. The resulting slice is returned. The function uses the fact that len and cap are legal when applied to the nil slice, and return 0.
Referenced from Effective Go
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
Variadic Functions and Slices
Functions can take an arbitrary number of final arguments. This is done using the ... syntax in the function signature.
A variadic function receives the variadic arguments as a slice.
func concat(strs ...string) string {
final := ""
// strs is just a slice of strings
for i := 0; i < len(strs); i++ {
final += strs[i]
}
return final
}
func main() {
final := concat("Hello ", "there ", "friend!")
fmt.Println(final)
// Output: Hello there friend!
}
The familiar fmt.Println() and fmt.Sprintf() are variadic, as are many in the standard library! fmt.Println() prints each element with space delimiters and a newline at the end.
func Println(a ...interface{}) (n int, err error)
Spread Operator
The spread operator allows us to pass a slice into a variadic function. The spread operator consists of three dots following the slice in the function call.
func printStrings(strings ...string) {
for i := 0; i < len(strings); i++ {
fmt.Println(strings[i])
}
}
func main() {
names := []string{"bob", "sue", "alice"}
printStrings(names...)
}
Append
The built-in append function is used to dynamically add elements to a slice:
func append(slice []Type, elems ...Type) []Type
If the underlying array is not large enough, append() will create a new underlying array and point the returned slice to it.
Notice that append() is variadic, the following are all valid:
slice = append(slice, oneThing)
slice = append(slice, firstThing, secondThing)
slice = append(slice, anotherSlice...)
Range
Go provides syntactic sugar to iterate easily over elements of a slice:
for INDEX, ELEMENT := range SLICE {
}
The element is a copy of the value at that index, and you can ignore returned values with an underscore _ instead of creating an unused variable. Range works on maps, strings, and channels too - we cover all of it in For Loops in Go.
Slice of Slices
Slices can hold other slices, effectively creating a matrix, or a 2D slice.
rows := [][]int{}
rows = append(rows, []int{1, 2, 3})
rows = append(rows, []int{4, 5, 6})
fmt.Println(rows)
// [[1 2 3] [4 5 6]]
Tricky Slices
The append() function changes the underlying array of its parameter AND returns a new slice. This means that using append() on anything other than itself is usually a BAD idea.
// don't do this!
someSlice = append(otherSlice, element)
Take a look at these head-scratchers:
Example 1: Works As Expected
a := make([]int, 3)
fmt.Println("len of a:", len(a))
fmt.Println("cap of a:", cap(a))
// len of a: 3
// cap of a: 3
b := append(a, 4)
fmt.Println("appending 4 to b from a")
fmt.Println("b:", b)
fmt.Println("addr of b:", &b[0])
// appending 4 to b from a
// b: [0 0 0 4]
// addr of b: 0x44a0c0
c := append(a, 5)
fmt.Println("appending 5 to c from a")
fmt.Println("addr of c:", &c[0])
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
// appending 5 to c from a
// addr of c: 0x44a180
// a: [0 0 0]
// b: [0 0 0 4]
// c: [0 0 0 5]
With slices a, b, and c, 4 and 5 seem to be appended as we would expect. We can even check the memory addresses and confirm that b and c point to different underlying arrays.
Example 2: Something Fishy
i := make([]int, 3, 8)
fmt.Println("len of i:", len(i))
fmt.Println("cap of i:", cap(i))
// len of i: 3
// cap of i: 8
j := append(i, 4)
fmt.Println("appending 4 to j from i")
fmt.Println("j:", j)
fmt.Println("addr of j:", &j[0])
// appending 4 to j from i
// j: [0 0 0 4]
// addr of j: 0x454000
g := append(i, 5)
fmt.Println("appending 5 to g from i")
fmt.Println("addr of g:", &g[0])
fmt.Println("i:", i)
fmt.Println("j:", j)
fmt.Println("g:", g)
// appending 5 to g from i
// addr of g: 0x454000
// i: [0 0 0]
// j: [0 0 0 5]
// g: [0 0 0 5]
In this example, however, when 5 is appended to i (creating g) it overwrites j's fourth index because j and g point to the same underlying array. The append() function only creates a new array when there isn't any capacity left. We created i with a length of 3 and a capacity of 8, which means we can append 5 items before a new array is automatically allocated.
Again, to avoid bugs like this, you should always use the append function on the same slice the result is assigned to:
mySlice := []int{1, 2, 3}
mySlice = append(mySlice, 4)
Strings and Slices
A string is really just a read-only slice of bytes.
Frequently Asked Questions
What is the difference between an array and a slice in Go?
Arrays are fixed in size. A slice is a dynamically-sized, flexible view of the elements of an array.
What is the zero value of a slice in Go?
The zero value of a slice is nil.
What do len and cap mean for a Go slice?
The length of a slice is the number of elements it contains. The capacity is the number of elements in the underlying array, counting from the first element in the slice.
When does append create a new underlying array?
If the underlying array is not large enough, append will create a new underlying array and point the returned slice to it.
Does range return the original slice element?
No. The element is a copy of the value at that index.
