Arrays & Slices
Samuel Grasse-Haroldsen
Posted on April 26, 2021
Arrays in Go
Arrays: one of the most fundamental data structures in computer science. An array is a collection of elements identifiable by an index. In Go that index begins at 0, not 1. Go arrays are limited to hold one type of data and cannot be resized. The array's length is part of it's type and cannot be changed. Another important aspect of arrays in Go is the fact they are passed by value, meaning they cannot be modified if passed to other functions. This may sound very limiting, but Go also has a data structure called a slice which adds functionality to Go arrays. We'll talk about slices in this article, but first the fundamentals!
Declaring an Array
//declare and then set values
var arr1 [2]string
arr1[0] = "Let's"
arr1[1] = "Rock"
//declare and initialize values in one step with a composite literal{} and the short assignment operator
arr2 := [4]int{5, 6, 4, 3}
//use ellipsis to have Go count the number of elements
arr3 := [...]float{
3.14,
4.13,
5.67,
43.55,
100.12,
1.0,
}
//the trailing comma is required when the composite literal is spread across multiple lines
The composite literal combined with the ellipsis gives us a simple, concise way to declare arrays.
Ranges
We can easily loop over elements of our array with a classic C-style for loop.
for i := 0; i < len(array); i++ {
fmt.Println(array[i])
}
But Go offers us a new reserved word that makes iterating through arrays simpler and less prone to out of bounds errors: range
.
//range in action
for i, name := range names {
fmt.Println(i, name)
}
In this example range gives us the index and name from each iteration of our array of names. If we don't care about the index, we can use the blank identifier (an underscore).
for _, age := range ages {
fmt.Println(age)
}
Passing Arrays to Functions
Two important things to remember: the length of an array is part of it's type and arrays are passed by value.
package main
import "fmt"
func paint(arr [2]string) {
for i, _ := range arr {
if i % 2 == 0 {
arr[i] = "Red " + arr[i]
} else {
arr[i] = "Green " + arr[i]
}
}
fmt.Println(arr) //outputs [Red House, Green Couch]
}
func main() {
arr := [2]{"House", "Couch"}
paint(arr)
fmt.Println(arr) //outputs [House, Couch]
}
In this function we can see that our arr
in main is not affected by the "painting" our paint
function executes and if we added an element to our arr
like this: arr := [3]{"House", "Couch", "Car"}
, we would also need to change our paint
function to accept arrays with type [3]string
.
Slices
Now that we are familiar with the limits of arrays, we can talk about versatile slices! Slices are views or windows into arrays. Slices can view an entire array or just a portion of it. Rob Pyke wrote an excellent article on slices that definitely clear things up. Here's a quick definition of slices straight from the article:
A slice is a data structure describing a contiguous section of an array stored separately from the slice variable itself. A slice is not an array. A slice describes a piece of an array.
Creating Slices from Arrays
Let's look at a quick example.
//declares a food array
food := [...]string{
"Carrot",
"Broccoli",
"Potato",
"Corn",
"Strawberry",
"Apple",
"Peach",
"Watermelon",
"Rice",
"Wheat",
"Amaranth",
}
//3 different slices, all pointing to different parts of the same food array
veggies := food[0:4]
fruits := food[4:8]
grains := food[8:]
In this example we are grabbing slices of our food array using half-open ranges. This means the first index specified in the range is included in the slice but the second is not. And in our grains slice declaration you can see that leaving off the final index lets the Go compiler know we want a slice from index 8 to the end of the array. We can do the same thing with the beginning of the array by leaving off the first index: veggies := food[:4]
or we can get a slice of the whole array by not specifying any indexes: foodSlice := food[:]
.
Declaring Slices
We can also declare slices without explicitly declaring an array first (the compiler creates an array for the slice to point to).
legumes := []string{"Soy", "Lentil", "Pea", "Peanut"}
The only difference from declaring an array with a composite literal and a slice is the empty [], because we don't need to specify a length on our slice.
Passing Slices to Functions
Unlike arrays, slices have the ability to be altered (and the underlying arrays they point to) in other functions. Also, because slices don't have a length tied to their type, different sized slices can be passed to the same function. Let's look at an example that will capitalize the first letter of our food array.
package main
import (
"fmt"
"strings"
)
func capWords(words []string) {
for i, w := range words {
words[i] = strings.Title(w)
}
}
func main() {
food := [...]string{
"carrot",
"broccoli",
"potato",
"corn",
"strawberry",
"apple",
"peach",
"watermelon",
"rice",
"wheat",
"amaranth",
}
veggies := food[0:4]
fruits := food[4:8]
grains := food[8:]
capWord(veggies)
capWord(fruits)
capWord(grains)
fmt.Println(food)
}
As you can guess, when we print our food array, it has been altered thanks to our slices and even though not all of the slices are the same length, the compiler does not complain! Yay slices!!
Appending to a Slice
Arrays can't grow, but slices can. This may seem strange considering our slices are just windows into our array. This is why it is important to know what is going on under the hood when we append to our slice. Here is a quick example that we can learn more from.
flowers := []string{"Ranunculus", "Daisy", "Daffodil"}
fmt.Printf("Length: %v Capacity: %v\n", len(flowers), cap(flowers))
flowers = append(flowers, "Tulip")
fmt.Printf("Length: %v Capacity: %v\n", len(flowers), cap(flowers))
Using the the built-in functions append
, cap
and len
, we can add elements to our slice of flowers and see how many elements flowers contains and how much room it has (the capacity). When we run this code, we can see that the length increased by 1 after appending "Tulip", but capacity changed in a not so predictable way. Initially set to 3 because of our 3 flowers and then it doubled to 6. Here is where we can deduce that Go made a new array with a larger capacity. This is how slices are able to grow! Go makes an array with a capacity for the initial elements and then copies those values to a new array when we append items to our slice. Neat!
NOTE: append
is a variadic function meaning it can take any number of items
Conclusion
Arrays give us the ability to keep track of collections of data. Slices allow us to work with arrays on a higher level of abstraction in order to do dynamic things with arrays!
Posted on April 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.