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.
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 int
s. 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:
First we create an array of length 5 to hold our test scores, then we fill up each element with a grade
Next we setup a for loop to compute the total score
Finally we divide the total score by the number of elements to find the average
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, }
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)]
.
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.
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 string
s to int
s.”
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/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.
How do you access the 4th element of an array or slice?
What is the length of a slice created using: make([]int, 3, 9)
?
Given the array:
x := [6]string{"a","b","c","d","e","f"}
what would x[2:5]
give you?
Write a program that finds the smallest number in this list:
x := []int{ 48,96,86,68, 57,82,63,70, 37,34,83,27, 19,97, 9,17, }
← Previous | Index | Next → |