left-icon

Go Succinctly®
by Mark Lewin

Previous
Chapter

of
A
A
A

CHAPTER 7

Arrays, Slices, and Maps

Arrays, Slices, and Maps


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

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

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 5effectively 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.

Maps

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)

} else {

     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

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.