CHAPTER 7
In Chapter 5, we looked at the basic data “primitives” that Go supports: numbers, strings, and Booleans. As programmers, we often want to deal with several such values as a single unit. Go provides a number of built-in “composite” types to help us do this. In this chapter, we will consider arrays, slices, and maps.
Arrays are a type of data structure that can store a fixed-size sequential collection of elements of the same type. The key thing to note here is that the length of an array is fixed. Once you have defined that, it will always hold exactly the same number of elements.
The way we declare an array in Go is to specify the name of the variable that holds the array, the number of elements that the array can store, and the data type of those elements. For example:
var nums [6]int // holds 6 integers
var strings [3]string // holds 3 strings
var preciseNums [10]float64 // holds 10 float64 numbers
We can retrieve the length of any array by using the len() function:
fmt.Printf("Length: %d\n" + len(nums)) // Displays "Length: 6"
If we want to initialize the items in each array at the same time as we declare them, we can do so by specifying the value of each item in a comma-separated list within braces:
totals := [5]int{1, 2, 3, 4, 5}
If you don’t know the length of the array or you’re just feeling lazy and want Go to work it out for you, you can use this notation:
totals := [...]int{1, 2, 3, 4, 5}
If we don’t initialize the array, each item in the array is initialized with the default value for its data type:
fmt.Println(nums) // Displays "[0 0 0 0 0 0]"
fmt.Println(strings) // Displays "[ ]"
fmt.Println(preciseNums) // Displays " [0 0 0 0 0 0 0 0 0 0]"
We can set and get the value of individual array items by referring to their index positions. As with the position of bytes within strings, the numbering of Go array indexes begins at zero. So the second item in the array is at position 1, the third item in the array is at position 2, and so on.
nums[4] = 50 // Sets the 5th item to 50
fmt.Println("nums[4] = " + nums[4]) // Displays "nums[4] = 50"
If we want to process each item in an array, we can use a for loop with the range keyword, specifying the name of a variable to store the individual array item during each loop iteration and the name of the array as a whole:
for _, item := range myArray {
// Process the array item
}
Consider the following example, which loops through an array containing rainfall statistics for the previous five years and calculates the average (mean) rainfall for the period.
Code Listing 15
package main import ( "fmt" ) func main() { total := 0 mean := 0 rainfallStats := [5]int{1091, 2010, 995, 1101, 1111} for _, value := range rainfallStats { total += value } mean = total / len(rainfallStats) fmt.Printf("Average rainfall: %d mm\n", mean) } |
Average rainfall: 1261 mm
Array types are one-dimensional, but you can combine array types to build multi-dimensional data structures. The following example creates an array with five rows and two columns, and outputs the value stored in each row:
Code Listing 16
package main import "fmt" func main() { // Array with 5 rows and 2 columns var arr = [5][2]int{ {0,0}, {2,4}, {1,3}, {5,7}, {6,8}} // Display each array element's value for i := 0; i < 5; i++ { for j := 0; j < 2; j++ { fmt.Printf("arr[%d][%d] = %d\n", i,j, arr[i][j] ) } } } |
arr[0][0] = 0
arr[0][1] = 0
arr[1][0] = 2
arr[1][1] = 4
arr[2][0] = 1
arr[2][1] = 3
arr[3][0] = 5
arr[3][1] = 7
arr[4][0] = 6
arr[4][1] = 8
Initializing big arrays in a single line of does not make your code easy to read, so Go allows you to put the elements on successive lines, as follows:
a := [5]int{
93,
79,
34,
202,
17,
}
Notice the last comma? Most languages would complain about that. Go actually requires it. Why? Because it makes it easier to comment out items that you no longer require without having to adjust the position of the last comma:
a := [5]int{
93,
79,
34,
//202,
17,
}
You’ve got to love Go—it’s always trying to make your life as a programmer easier!
However, we now have an array with space for five items that only contains four. If we print this out, we can see that Go has moved everything up and substituted a default value for the empty item in the list:
fmt.Println(a) // Displays "[93 79 34 17 0]"
This is not necessarily what we want, and one of several reasons that you don’t often see arrays being used in Go programs. They can be a bit of a pain to work with, especially when it comes to resizing them—you can’t! The only way around that is to create a new array.
Tip: Make sure you really want an array before you use one. Arrays are limited, and don’t have as much flexibility as their much more flexible cousins the slices, described later in this chapter.
Another thing to note about arrays is that when you pass one into a function as a parameter, what the function receives is not the array itself, but a copy of the array. Any changes you make to the array in the function will not affect the original array. This is known as passing by value, and can be computationally expensive if you are dealing with a very large array.
The following example demonstrates this:
Code Listing 17
package main import ( "fmt" ) func main() { myArray := [...]string{"Apples", "Oranges", "Bananas"} fmt.Printf("Initial array values: %v\n", myArray) myFunction(myArray) fmt.Printf("Final array values: %v\n", myArray) } func myFunction(arr [3]string) { // Change Oranges to Strawberries arr[1] = "Strawberries" fmt.Printf("Array values in myFunction(): %v\n", arr) } |
Initial array values: [Apples Oranges Bananas]
Array values in myFunction(): [Apples Strawberries Bananas]
Final array values: [Apples Oranges Bananas]
Like other languages, Go lets you pass by value or by reference. The difference is that when you pass by reference, you pass a pointer to the memory location where the value is stored, so that you are working with the original object. If the function changes the value of that object, the changes are permanent once the function has finished executing. We’ll talk about pointers in Chapter 9 when we discuss user-defined types, but bear this in mind when choosing to use an array in Go.
So, you can now see that the array type has its limitations. That’s not to say we’ve wasted time covering it here, because it leads us into talking about another, more capable composite type based on the array, called a slice.
Slices are a key data type in Go. They do everything you expect an array to do, and more.
A slice is basically a “view” into an underlying array.
You can create and populate a slice simply by defining an array without specifying the size:
fruits := []string{"Apples", "Oranges", "Bananas", "Kiwis"}
This approach provides several benefits.
One is that you can refer to a range of items in the slice using a similar notation as you used with substrings in Chapter 5—effectively taking a “subslice” from the slice.
The syntax to use is:
[lower-bound, upper-bound]
… where lower-bound is included in the subslice and upper-bound is excluded from it.
For example:
Code Listing 18
package main import ( "fmt" ) func main() { fruits := [...]string{"apples", "oranges", "bananas", "kiwis"} fmt.Printf("%v\n", fruits[1:3]) fmt.Printf("%v\n", fruits[0:2]) fmt.Printf("%v\n", fruits[:3]) fmt.Printf("%v\n", fruits[2:]) } |
[oranges bananas]
[apples oranges]
[apples oranges bananas]
[bananas kiwis]
Another benefit is that when you pass a slice to a function as a parameter, instead of the function receiving a copy of the underlying array it gets a pointer to it. That means anything you do to the slice within the function is reflected in the underlying array. We can demonstrate this by rewriting the program in Code Listing 17 to use a slice instead of an array.
Code Listing 19
package main import ( "fmt" ) func main() { mySlice := []string{"Apples", "Oranges", "Bananas"} fmt.Printf("Initial slice values: %v\n", mySlice) myFunction(mySlice) fmt.Printf("Final slice values: %v\n", mySlice) } func myFunction(fruits []string) { // Change Oranges to Strawberries fruits[1] = "Strawberries" fmt.Printf("Slice values in myFunction(): %v\n", fruits) } |
Initial slice values: [Apples Oranges Bananas]
Slice values in myFunction(): [Apples Strawberries Bananas]
Final slice values: [Apples Strawberries Bananas]
So far we have created slices directly from the items we want to store in them. If you want to create a new slice and then add data to it later, you need to use the built-in make() function.
mySlice := make([]int)
If you know the initial size, you can specify it as follows:
mySlice := make([]int, 4)
You can also specify a maximum size by supplying a third parameter to the make() function and retrieve its value at any time by calling cap(). The following code creates a slice that will store up to eight int values, with an initial capacity of four such values:
Code Listing 20
package main import ( "fmt" ) func main() { mySlice := make([]int, 4, 8) fmt.Printf("Initial Length: %d\n", len(mySlice)) fmt.Printf("Capacity: %d\n", cap(mySlice)) fmt.Printf("Contents: %v\n", mySlice) } |
Initial Length: 4
Capacity: 8
Contents: [0 0 0 0]
If we only know the maximum number of items our slice should be able to hold (eight), but not the number of initial values, we could create it like this:
mySlice := make([]int, 0, 8)
Let’s populate our slice:
mySlice[0] = 1
mySlice[1] = 3
mySlice[2] = 5
mySlice[3] = 7
That’s a rather cumbersome way of doing it, so let’s use append() instead:
mySlice = append(mySlice, 1, 3, 5, 7)
The main benefit of a slice is that it can be resized dynamically. Let’s say we have specified a capacity for the slice, but we’ve added more items than the capacity allows. What happens?
Let’s see:
Code Listing 21
package main import ( "fmt" ) func main() { mySlice := make([]int, 0, 8) mySlice = append(mySlice, 1, 3, 5, 7, 9, 11, 13, 17) fmt.Printf("Contents: %v\n", mySlice) fmt.Printf("Number of Items: %d\n", len(mySlice)) fmt.Printf("Capacity: %d\n", cap(mySlice)) mySlice = append(mySlice, 19) fmt.Printf("Contents: %v\n", mySlice) fmt.Printf("Number of Items: %d\n", len(mySlice)) fmt.Printf("Capacity: %d\n", cap(mySlice)) } |
Contents: [1 3 5 7 9 11 13 17]
Number of Items: 8
Capacity: 8
Contents: [1 3 5 7 9 11 13 17 19]
Number of Items: 9
Capacity: 16
What happened here is that when we appended the ninth item to the slice, Go automatically doubled the capacity for us in order to make extra room in case we want to continue adding items. So the capacity we specified when we created the slice is not treated as a hard limit, just a guideline for Go to determine the initial memory allocation.
However, this only works because we used the slice-specific append() function. If we had attempted to add the ninth integer using the standard array approach, we would receive a run-time error:
Code Listing 22
package main import ( "fmt" ) func main() { mySlice := make([]int, 0, 8) mySlice = append(mySlice, 1, 3, 5, 7, 9, 11, 13, 17) fmt.Printf("Contents: %v\n", mySlice) fmt.Printf("Number of Items: %d\n", len(mySlice)) fmt.Printf("Capacity: %d\n", cap(mySlice)) mySlice[8] = 19 fmt.Printf("Contents: %v\n", mySlice) fmt.Printf("Number of Items: %d\n", len(mySlice)) fmt.Printf("Capacity: %d\n", cap(mySlice)) } |
Contents: [1 3 5 7 9 11 13 17]
Number of Items: 8
Capacity: 8
panic: runtime error: index out of range
goroutine 1 [running]:
panic(0xda900, 0xc82000a080)
/usr/local/go/src/runtime/panic.go:464 +0x3e6
main.main()
.../src/hello/main.go:17 +0x725
exit status 2
This is because index position 8 does not exist. It’s only by using append() that Go can detect that we need extra capacity and then provide it.
Finally, I mentioned that one of the benefits of slices is that you can pass them into functions as pointers to their underlying arrays and therefore make changes to the original (and not just a copy), which is what happens when you pass an array to a function.
But what if you genuinely want to take a copy of a slice? You can do that with Go’s copy() function:
Code Listing 23
package main import ( "fmt" ) func main() { mySlice := make([]int, 0, 8) mySlice = append(mySlice, 1, 3, 5, 7, 9, 11, 13, 17) mySliceCopy := make([]int, 8) copy(mySliceCopy, mySlice) mySliceCopy[3] = 999 fmt.Printf("mySlice: %v\n", mySlice) fmt.Printf("mySliceCopy: %v\n", mySliceCopy) } |
mySlice: [1 3 5 7 9 11 13 17]
mySliceCopy: [1 3 5 999 9 11 13 17]
Hopefully you can now see some of the immense advantages there are to working with slices compared to arrays. For these reasons, it’s actually quite rare to see arrays in Go: slices are almost always the best choice.
A map is an unordered collection of key-value pairs. If you’ve been programming for a while, you’ve seen this kind of structure referred to elsewhere as an associative array, a dictionary, or a hash.
The idea behind a map is that you access its values by referencing the key that points to each value. The keys can be any data type that you can test for equality. So strings, integers, floating-point numbers, and so on are all good, but you cannot use an array as a key, for example.
Maps are often used as a kind of in-memory “lookup” table. Say for instance that we want to keep a record of famous actors and their ages. We can create a map using the make() function, just as we did with a slice:
actor := make(map[string]int)
In the map declaration, the type within square brackets is the type of data we want to use for the keys, and the type following it is the data type of the values.
We then assign an item to the map using the following syntax:
actor["Redford"]= 79
We then retrieve it by specifying the key of the value we want to access:
fmt.Println(actor["Redford"])
Code Listing 24
package main import "fmt" func main() { actor := make(map[string]int) actor["Paltrow"] = 43 actor["Cruise"] = 53 actor["Redford"] = 79 actor["Diaz"] = 43 actor["Kilmer"] = 56 actor["Pacino"] = 75 actor["Ryder"] = 44 fmt.Printf("Robert Redford is %d years old\n", actor["Redford"]) fmt.Printf("Cameron Diaz is %d years old\n", actor["Diaz"]) fmt.Printf("Val Kilmer is %d years old\n", actor["Kilmer"]) } |
Robert Redford is 79 years old
Cameron Diaz is 43 years old
Val Kilmer is 56 years old
Generously, Go also provides us with a shorthand syntax for creating maps:
Code Listing 25
package main import "fmt" func main() { actor := map[string]int{ "Paltrow": 43, "Cruise": 53, "Redford": 79, "Diaz": 43, "Kilmer": 56, "Pacino": 75, "Ryder": 44, } fmt.Printf("Robert Redford is %d years old\n", actor["Redford"]) fmt.Printf("Cameron Diaz is %d years old\n", actor["Diaz"]) fmt.Printf("Val Kilmer is %d years old\n", actor["Kilmer"]) } |
Robert Redford is 79 years old
Cameron Diaz is 43 years old
Val Kilmer is 56 years old
If we try to access a map value with a nonexistent key, Go will return the default value for the data type:
fmt.Printf("Anthony Hopkins is %d years old\n", actor["Hopkins"])
This results in:
Anthony Hopkins is 0 years old
In our example, that’s unlikely to cause much confusion: we can simply test for zero to see if our users have specified a valid key. But what if zero is a legitimate value? How can we differentiate between that and an invalid key?
The solution is to use the “comma OK” syntax. This takes advantage of the map’s ability to return two values instead of one when we attempt to retrieve an item. The first (age) is the value itself, and the second (ok) is a Boolean that tells us whether or not the key used was valid:
if age, ok := actor["Hopkins"]; ok {
fmt.Printf("Anthony Hopkins is %d years old\n", age)
fmt.Println("Actor not recognized.")
Tip: You can use an initialization statement to declare a variable in the same line as an if statement. This leads to clean, understandable code. The variable's scope is the statement in which it is defined.
You can iterate through a map using a for loop with the range statement, just as we did with an array. Note, however, that the items in a map will be unordered, so you cannot rely on them being in the order you inserted them, or indeed in the same order each time you loop through them. This is by design: the Go team didn’t want programmers to rely upon an ordering that was essentially unreliable, so they randomized the iteration order to make that impossible.
Code Listing 26
package main import "fmt" func main() { actor := map[string]int{ "Paltrow": 43, "Cruise": 53, "Redford": 79, "Diaz": 43, "Kilmer": 56, "Pacino": 75, "Ryder": 44, } for i := 1; i < 4; i++ { fmt.Printf("\nRUN NUMBER %d\n", i) for key, value := range actor { fmt.Printf("%s : %d years old\n", key, value) } } } |
RUN NUMBER 1
Ryder : 44 years old
Paltrow : 43 years old
Cruise : 53 years old
Redford : 79 years old
Diaz : 43 years old
Kilmer : 56 years old
Pacino : 75 years old
RUN NUMBER 2
Kilmer : 56 years old
Pacino : 75 years old
Ryder : 44 years old
Paltrow : 43 years old
Cruise : 53 years old
Redford : 79 years old
Diaz : 43 years old
RUN NUMBER 3
Paltrow : 43 years old
Cruise : 53 years old
Redford : 79 years old
Diaz : 43 years old
Kilmer : 56 years old
Pacino : 75 years old
Ryder : 44 years old
If you want to sort the items in a map, you must rely on another structure for help. The following example demonstrates pulling the actor keys into a slice, ordering the slice using the sort package, then iterating through the slice to retrieve the actor names from the map in alphabetical order.
Code Listing 27
package main import ( "fmt" "sort" ) func main() { actor := map[string]int{ "Paltrow": 43, "Cruise": 53, "Redford": 79, "Diaz": 43, "Kilmer": 56, "Pacino": 75, "Ryder": 44, } // Store the keys in a slice var sortedActor []string for key := range actor { sortedActor = append(sortedActor, key) } // Sort the slice alphabetically sort.Strings(sortedActor) /* Retrieve the keys from the slice and use them to look up the map values */ for _, name := range sortedActor { fmt.Printf("%s : %d years old\n", name, actor[name]) } } |
Cruise : 53 years old
Diaz : 43 years old
Kilmer : 56 years old
Pacino : 75 years old
Paltrow : 43 years old
Redford : 79 years old
Ryder : 44 years old