Arrays, Slices and Maps

In chapter 3 we learned about Go's basic types. In this chapter we will look at three more built-in types: arrays, slices and maps.

Arrays

An array is a numbered sequence of elements of a single type with a fixed length. In Go they look like this:

var x [5]int

x is an example of an array which is composed of 5 ints. Try running the following program:

package main

import "fmt"

func main() {
    var x [5]int
    x[4] = 100
    fmt.Println(x)
}

You should see this:

[0 0 0 0 100]

x[4] = 100 should be read “set the 5th element of the array x to 100”. It might seem strange that x[4] represents the 5th element instead of the 4th but like strings, arrays are indexed starting from 0. Arrays are accessed in a similar way. We could change fmt.Println(x) to fmt.Println(x[4]) and we would get 100.

Here's an example program that uses arrays:

func main() {
    var x [5]float64
    x[0] = 98
    x[1] = 93
    x[2] = 77
    x[3] = 82
    x[4] = 83
    
    var total float64 = 0
    for i := 0; i < 5; i++ {
        total += x[i]
    }
    fmt.Println(total / 5)
}

This program computes the average of a series of test scores. If you run it you should see 86.6. Let's walk through the program:

This program works, but Go provides some features we can use to improve it. First these 2 parts: i < 5 and total / 5 should throw up a red flag for us. Say we changed the number of grades from 5 to 6. We would also need to change both of these parts. It would be better to use the length of the array instead:

    var total float64 = 0
    for i := 0; i < len(x); i++ {
        total += x[i]
    }
    fmt.Println(total / len(x))

Go ahead and make these changes and run the program. You should get an error:

$ go run tmp.go
# command-line-arguments
.\tmp.go:19: invalid operation: total / 5 (mismatched types float64 and int)

The issue here is that len(x) and total have different types. total is a float64 while len(x) is an int. So we need to convert len(x) into a float64:

fmt.Println(total / float64(len(x)))

This is an example of a type conversion. In general to convert between types you use the type name like a function.

Another change to the program we can make is to use a special form of the for loop:

var total float64 = 0
for i, value := range x {
    total += value
}
fmt.Println(total / float64(len(x)))

In this for loop i represents the current position in the array and value is the same as x[i]. We use the keyword range followed by the name of the variable we want to loop over.

Running this program will result in another error:

$ go run tmp.go
# command-line-arguments
.\tmp.go:16: i declared and not used

The Go compiler won't allow you to create variables that you never use. Since we don't use i inside of our loop we need to change it to this:

var total float64 = 0
for _, value := range x {
    total += value
}
fmt.Println(total / float64(len(x)))

A single _ (underscore) is used to tell the compiler that we don't need this. (In this case we don't need the iterator variable)

Go also provides a shorter syntax for creating arrays:

x := [5]float64{ 98, 93, 77, 82, 83 }

We no longer need to specify the type because Go can figure it out. Sometimes arrays like this can get too long to fit on one line, so Go allows you to break it up like this:

x := [5]float64{ 
    98, 
    93, 
    77, 
    82, 
    83,
}

Notice the extra trailing , after 83. This is required by Go and it allows us to easily remove an element from the array by commenting out the line:

x := [4]float64{ 
    98, 
    93, 
    77, 
    82, 
    // 83,
}

Slices

A slice is a segment of an array. Like arrays slices are indexable and have a length. Unlike arrays this length is allowed to change. Here's an example of a slice:

 var x []float64

The only difference between this and an array is the missing length between the brackets. In this case x has been created with a length of 0.

If you want to create a slice you should use the built-in make function:

x := make([]float64, 5)

This creates a slice that is associated with an underlying float64 array of length 5. Slices are always associated with some array, and although they can never be longer than the array, they can be smaller. The make function also allows a 3rd parameter:

x := make([]float64, 5, 10)

10 represents the capacity of the underlying array which the slice points to:

Another way to create slices is to use the [low : high] expression:

arr := [5]float64{1,2,3,4,5}
x := arr[0:5]

low is the index of where to start the slice and high is the index where to end it (but not including the index itself). For example while arr[0:5] returns [1,2,3,4,5], arr[1:4] returns [2,3,4].

For convenience we are also allowed to omit low, high or even both low and high. arr[0:] is the same as arr[0:len(arr)], arr[:5] is the same as arr[0:5] and arr[:] is the same as arr[0:len(arr)].

Slice Functions

Go includes two built-in functions to assist with slices: append and copy. Here is an example of append:

func main() {
    slice1 := []int{1,2,3}
    slice2 := append(slice1, 4, 5)
    fmt.Println(slice1, slice2)
}

After running this program slice1 has [1,2,3] and slice2 has [1,2,3,4,5]. append creates a new slice by taking an existing slice (the first argument) and appending all the following arguments to it.

Here is an example of copy:

func main() {
    slice1 := []int{1,2,3}
    slice2 := make([]int, 2)
    copy(slice2, slice1)
    fmt.Println(slice1, slice2)
}

After running this program slice1 has [1,2,3] and slice2 has [1,2]. The contents of slice1 are copied into slice2, but since slice2 has room for only two elements only the first two elements of slice1 are copied.

Maps

A map is an unordered collection of key-value pairs. Also known as an associative array, a hash table or a dictionary, maps are used to look up a value by its associated key. Here's an example of a map in Go:

var x map[string]int

The map type is represented by the keyword map, followed by the key type in brackets and finally the value type. If you were to read this out loud you would say “x is a map of strings to ints.”

Like arrays and slices maps can be accessed using brackets. Try running the following program:

var x map[string]int
x["key"] = 10
fmt.Println(x)

You should see an error similar to this:

panic: runtime error: assignment to entry in nil map

goroutine 1 [running]:
main.main()
  main.go:7 +0x4d

goroutine 2 [syscall]:
created by runtime.main
        C:/Users/ADMINI~1/AppData/Local/Temp/2/bindi
t269497170/go/src/pkg/runtime/proc.c:221
exit status 2

Up till now we have only seen compile-time errors. This is an example of a runtime error. As the name would imply, runtime errors happen when you run the program, while compile-time errors happen when you try to compile the program.

The problem with our program is that maps have to be initialized before they can be used. We should have written this:

x := make(map[string]int)
x["key"] = 10
fmt.Println(x["key"])

If you run this program you should see 10 displayed. The statement x["key"] = 10 is similar to what we saw with arrays but the key, instead of being an integer, is a string because the map's key type is string. We can also create maps with a key type of int:

x := make(map[int]int)
x[1] = 10
fmt.Println(x[1])

This looks very much like an array but there are a few differences. First the length of a map (found by doing len(x)) can change as we add new items to it. When first created it has a length of 0, after x[1] = 10 it has a length of 1. Second maps are not sequential. We have x[1], and with an array that would imply there must be an x[0], but maps don't have this requirement.

We can also delete items from a map using the built-in delete function:

delete(x, 1)

Let's look at an example program that uses a map:

package main

import "fmt"

func main() {
    elements := make(map[string]string)
    elements["H"] = "Hydrogen"
    elements["He"] = "Helium"
    elements["Li"] = "Lithium"
    elements["Be"] = "Beryllium"
    elements["B"] = "Boron"
    elements["C"] = "Carbon"
    elements["N"] = "Nitrogen"
    elements["O"] = "Oxygen"
    elements["F"] = "Fluorine"
    elements["Ne"] = "Neon"
    
    fmt.Println(elements["Li"])
}

elements is a map that represents the first 10 chemical elements indexed by their symbol. This is a very common way of using maps: as a lookup table or a dictionary. Suppose we tried to look up an element that doesn't exist:

fmt.Println(elements["Un"])

If you run this you should see nothing returned. Technically a map returns the zero value for the value type (which for strings is the empty string). Although we could check for the zero value in a condition (elements["Un"] == "") Go provides a better way:

name, ok := elements["Un"]
fmt.Println(name, ok)

Accessing an element of a map can return two values instead of just one. The first value is the result of the lookup, the second tells us whether or not the lookup was successful. In Go we often see code like this:

if name, ok := elements["Un"]; ok {    
    fmt.Println(name, ok)
}

First we try to get the value from the map, then if it's successful we run the code inside of the block.

Like we saw with arrays there is also a shorter way to create maps:

elements := map[string]string{
    "H": "Hydrogen",
    "He": "Helium",
    "Li": "Lithium",
    "Be": "Beryllium",
    "B": "Boron",
    "C": "Carbon",
    "N": "Nitrogen",
    "O": "Oxygen",
    "F": "Fluorine",
    "Ne": "Neon",
}

Maps are also often used to store general information. Let's modify our program so that instead of just storing the name of the element we store its standard state (state at room temperature) as well:

func main() {
    elements := map[string]map[string]string{
        "H": map[string]string{
            "name":"Hydrogen", 
            "state":"gas",
        },
        "He": map[string]string{
            "name":"Helium", 
            "state":"gas",
        },
        "Li": map[string]string{
            "name":"Lithium", 
            "state":"solid",
        },
        "Be": map[string]string{
            "name":"Beryllium", 
            "state":"solid",
        },
        "B":  map[string]string{
            "name":"Boron",
            "state":"solid",
        },
        "C":  map[string]string{
            "name":"Carbon",
            "state":"solid",
        },
        "N":  map[string]string{
            "name":"Nitrogen",
            "state":"gas",
        },
        "O":  map[string]string{
            "name":"Oxygen",
            "state":"gas",
        },
        "F":  map[string]string{
            "name":"Fluorine",
            "state":"gas",
        },
        "Ne":  map[string]string{
            "name":"Neon",
            "state":"gas",
        },
    }

    if el, ok := elements["Li"]; ok {    
        fmt.Println(el["name"], el["state"])
    }
}

Notice that the type of our map has changed from map[string]string to map[string]map[string]string. We now have a map of strings to maps of strings to strings. The outer map is used as a lookup table based on the element's symbol, while the inner maps are used to store general information about the elements. Although maps are often used like this, in chapter 9 we will see a better way to store structured information.

Problems

← PreviousIndexNext →