This chapter covers the basics of maps and control structures. We explain how maps are a powerful tool for storing key-value pairs. This chapter also covers control structures such as if
and switch
statements.
Maps are a powerful built-in data structure that associates keys and values. A map is an unordered set of values indexed by a unique key. Map keys and values can be set and retrieved using the []
syntax. In Listing 4.1, we create a new map, map[string]string{}
, and assign it to the users
variable. To assign a value, we can access the key using the users[<key>]
syntax. In this case, we are accessing the key, Janis
, for assignment. To retrieve the Janis
key from the map, we can reverse the process. If we move the users[<key>]
syntax to the right of the assignment operator, we are now accessing the value of the key for reading.
Listing 4.1 Setting and Retrieving Map Values
You can use built-in len
1 function to find the length (the number of keys) of a map. In Listing 4.2, we assigning 4
values into the users
map. We then use the len
function to retrieve the number of keys in the map. As shown in the output in Listing 4.2, the len
function is correctly reporting the length of the users
map as 4.
1. https://pkg.go.dev/builtin#len
Listing 4.2 Using the len
Function to Find the Length of a Map
Theoretically, maps are able to hold an infinite number of keys. Because maps have unlimited capacity, the built-in cap
function cannot be used to find the capacity of a map. Listing 4.3 shows that trying to call the cap
function with a map results in a compilation error informing us that a map is an invalid argument for the cap
function.
Listing 4.3 Maps Have an “Unlimited” Capacity
You can initialize maps in a couple of ways. The first, and recommended, way is to initialize the map at the same time as declaring the variable, as in Listing 4.4. This also allows for initializing the map with an initial set of keys and values.
Listing 4.4 Initializing a Map with a Short Syntax
You also can use the make
2 function to create new maps, as in Listing 4.5. Unlike slices and arrays, maps cannot be initialized with a length and capacity. Older code may use the make
function; however, it is considered nonidiomatic in most circumstances to use the make
function for map initialization.
2. https://pkg.go.dev/builtin#make
Listing 4.5 Initializing a Map with the make
Function
If you don’t initialize a map and try to access the values, you will receive a runtime panic
, as shown in Listing 4.6.
Listing 4.6 Accessing an Uninitialized Map
Map keys must be comparable, which means the Go runtime can check the equality of the key in the map with the key being given. In Go, not all types are comparable. Basic data types, such as string
, []byte
, and int
, are the most-used key types. All are comparable and provide enough type variation to handle most use cases.
Complex and noncomparable types—such as functions (Listing 4.7), maps, or slices—cannot be used as key types in maps. Use of noncomparable types causes a compile time error.
Listing 4.7 Using a Noncomparable Type as a Map Key
A struct whose fields are all simple comparable types can be used as a key type for a map. This can be useful for creating tuple-style keys. In Listing 4.8, because the Simple
struct is composed of two fields, ID
and Name
, both of which are comparable types, the Simple
struct can be used as the key type for a map.
Listing 4.8 Using a Struct with Simple Fields as a Map Key
Structs that contain noncomparable fields, however, cannot be used as the key type for a map. Trying to do so causes a compilation error. In Listing 4.9, the Complex
struct type is composed of two fields: Data
and Fn
. Both of these fields are complex noncomparable types. The Fn
field, for example, is a function, and functions cannot be compared.
Listing 4.9 Using a Struct with Noncomparable Fields as a Map Key
Maps can be iterated over using the range
keyword, in much the same way that arrays and slices can. The range
keyword, in the case of maps, returns the key
and the value
for each entry in the map. In Listing 4.10, using the range
keyword to iterate over the users
map, we are given a single key/value pair, which are assigned to the key
and value
variables.
Listing 4.10 Iterating over a Map
Range returns the key and the value of each item in the map. If only the key for each loop is needed, and not the value, using only a single variable in the for
loop with range
returns only the key in the map. In Listing 4.11, instead of asking for both the key and value for each iteration of the map, we just ask for the key. We can then use that key to access the value from the map users[key]
.
Listing 4.11 Iterating over a Map with Only the Key
You can use the built-in delete
3 function, Listing 4.12, to remove a key, and its value, from a map.
3. https://pkg.go.dev/builtin#delete
Listing 4.12 The delete
Function
Only one key at a time can be deleted from a map. In Listing 4.13, we call the delete
function passing in the users
map and the key Kurt
. The result, when printed, is a map that no longer contains the Kurt
key.
Listing 4.13 Deleting a Key from a Map
If the key is not present in the map, the delete
function is a no-op, and the map will not be modified. In Listing 4.14, trying to delete a nonexistent key, Unknown
, from the users
map neither returns an error nor raises a panic. There is no indication as to the success or failure of calling the delete
function.
Listing 4.14 Deleting a Key from a Map That Is Not Present
When asking for a key from a map that doesn’t exist, Go returns the zero of the map’s value type. This can often lead to bugs in code.
In Listing 4.15, you see that even if you don’t get an error, you still have a bug because you didn’t check for the existence of the value.
Listing 4.15 Getting a Nonexistent Map Key
Maps in Go return an optional second boolean argument that tells you if the key exists in the map. Checking for the existence of the key in your code can help you avoid bugs caused by nonexistent keys.
In Listing 4.16, we ask for the optional second boolean argument when retrieving a value from a map. We then use that boolean, ok
, to handle the case where the key is not present in the map.
Listing 4.16 Checking for the Existence of a Key in a Map
There may be times when the zero value is OK. Consider Listing 4.17 and the task of counting the occurrences of words in a string. To store the count of each word, you can use a map with the key type string
and the value type int
.
When asking this map for a key that doesn’t exist, the zero value of the map’s value type is returned. The zero value of an int
is 0
, which is a valid starting point for counting a word’s occurrences.
Listing 4.17 Counting the Occurrences of Words in a String
Integers in Go can be incremented and decremented in place, using the ++
or --
operators, respectively. This means our code could be simplified because the act of incrementing the zero value of a missing map key causes the key/value pair to be added to the map. For example, in Listing 4.18, we are incrementing a map key’s value in-place using the ++
operator.
Listing 4.18 Using ++
to Increment a Map Value in Place
When retrieving a value from a map, you can use the second, optional, boolean argument to test whether the key exists in the map. To do this, however, you must also capture the value. There may be times when the value is not needed. In this case, you can use the _
to discard the value. For example, in Listing 4.19, when asking for the key foo
from the map, we can discard the value by using the underscore (_
) operator and only retain the second boolean value to the exists
variable.
Listing 4.19 Testing the Presence of a Key in a Map
Storing complex values, such as a struct, in a map is a very common practice. However, updating those complex values is not as straightforward as updating a simple value, like an int
.
It may seem intuitive to simply assign a new value to a struct via a map lookup, like we did with an int
in Listing 4.17. However, this is not the case. In Listing 4.20, you see that trying to update a struct in place causes a compilation error.
Listing 4.20 Compilation Error Trying to Update a Struct in a Map
When inserting a value into a map, the value is copied, Listing 4.21. This means that changes made to the original value after insertion aren’t reflected in the value stored in the map.
Listing 4.21 Values Inserted into a Map Are Copied
When updating a complex value in a map, Listing 4.22, that value must first be retrieved from the map. Once retrieved, the value can be updated. After the changes have been made, the value needs to be reinserted into the map for the changes to take effect.
Listing 4.22 Updating Complex Values in a Map
Go does not provide a way to get just a list of keys or values from a map. To build a list of keys or values, Listing 4.23, you must iterate over the map and save the keys or values to a slice or array.
Listing 4.23 Listing Keys from a Map
In Go, maps are not ordered and there are no built-in methods for sorting maps. When iterating over a map, like in Listing 4.23, the order of the keys is not guaranteed.
To sort a map, you must first get the keys from the map, sort the keys, and then use the sorted keys to retrieve the values from the map. The sort
4 package, Listing 4.24, provides a number of functions and interfaces for sorting collection types.
Listing 4.24 The sort
Package
In Listing 4.25, after iterating over the map and building a slice of keys, we can sort those keys in place using the sort.Ints
5 function. With a sorted list of keys, we can then iterate over the keys and retrieve the values from the map. The result, as shown in Listing 4.25, is a sorted list of values.
5. https://pkg.go.dev/sort#Ints
Listing 4.25 Sorting Keys and Retrieving Values from a Map
If statements are the core way that most programming languages use to make logic decisions. The if
statement in Go is like most other languages, with a few extra syntax options.
In Listing 4.26, we use an equality comparison, the ==
operator, to determine whether the greet
variable is equal to true
. If it is, we print the hello
string.
Listing 4.26 A Basic Boolean Logic Check
Due to the way that Go interprets the expression, you can use anything that evaluates to true
. Listing 4.26 can be rewritten to remove the comparison against true
. Listing 4.27 functions identically to Listing 4.26, but it does so by just using the single variable greet
that evaluates to true
or false
.
Listing 4.27 A Basic if
Statement
else
StatementWe can also make use of the else
statement, Listing 4.28. This allows us to program for both the true
and false
state of our expression.
Listing 4.28 An if
Statement with an else
Statement
Although Listing 4.28 is valid code, in Go, it is considered best practice to avoid using the else
statement whenever possible. A common use case is to use what is referred to as an “early return.” Listing 4.29 is functionally equivalent to Listing 4.28, but it uses an early return to avoid using the else
keyword. The typical result is clearer code.
Listing 4.29 An if
Statement with an Early Return
else if
StatementWhen necessary, you can make use of the else if
syntax, Listing 4.30. This allows you to evaluate several different expressions in one if
statement.
Listing 4.30 An if
Statement with an else if
Statement
Listing 4.30 could also be written with early returns as well as nested if
statements. This would result in simpler code that is both more readable and less likely to introduce bugs later on in refactoring.
A common operation in Go is to look up a value in a map
. When doing this, it is important to check that the item was found in the map. You do this by asking for the second optional boolean value.
In Listing 4.31, the name is looked up, and then an if
statement is used to validate that the name was found.
Although doing the lookup and the logic test in two lines of code is fine, it does have one disadvantage, which is that the scope of the age
and ok
variables are available throughout the entire main
function. From a code quality standpoint, any time you can reduce variable scope, you are likely reducing future bugs in your code.
Listing 4.31 Scoping Variables to an if
Statement
As such, Go also allows you to create a simple statement before the expression is evaluated. You can use this approach when performing map lookups, as in Listing 4.31, or for other operations that require setting a local variable only for use in the logic expression of the if
statement.
To make use of this syntax, you can first write the assignment statement and then use the semicolon followed by the logic expression.
One of the primary advantages of creating the assignment as part of the if
statement is that it scopes those variables to the if
statement. In Listing 4.32, the age
and ok
variables are no longer available in the entire scope of the main
function but only available within the scope of the if
statement. Any time you can reduce the scope of where a variable is used, it results in better code quality because you are less likely to introduce a bug.
Listing 4.32 Scoping Variables to an if
Statement
When dealing with logic control statements such as the if
statement, understanding how logic operators work can help simplify your code.
While there are several operators, we can break them down into four categories: boolean, mathematical, logical, and bitwise. Tables 4.1 through 4.4 outline these categories.
Table 4.1 Boolean Operators
Operator | Description |
---|---|
&& | Conditional AND |
|| | Conditional OR |
! | NOT |
Table 4.2 Mathematical Operators
Operator | Description |
---|---|
+ | Sum |
- | Difference |
* | Product |
/ | Quotient |
% | Remainder (modulus) |
Table 4.3 Logic Comparison
Operator | Description |
---|---|
== | Equals |
!= | Not equal |
< | Less than |
<= | Less than or equal |
> | Greater than |
>= | Greater than or equal |
Table 4.4 Bitwise Operators
Operator | Description |
---|---|
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
&^ | Bit clear (AND NOT) |
« | Left shift |
» | Right shift |
Switch
StatementsSwitch
statements allow for the same type of logic decisions as if
statements but tend to be easier to read.
Consider Listing 4.33. The large amount of if/else if
statements make it difficult to read and maintain.
Listing 4.33 A Complex Set of if/else if
Statements
The switch
statement is a more compact way to write the same logic. In Listing 4.34, we are able to replace the if/else if
statements with a switch
statement and use the case
keyword to evaluate the expression. Each case
statement is evaluated in order, and the first one that evaluates to true
is used.
Listing 4.34 A Long Version of a switch
Statement
In Listing 4.34, there is no expression
provided to the switch
statement. However, the switch
statement allows for the use of an expression
on the initial line. This removes the need to repeat the month ==
part of the statement on each line. In Listing 4.35, we use the switch
statement to evaluate the month
variable. Each case
statement can then be
simplified to case N
, where N
is the number of the month.
Listing 4.35 The switch
Statement with a Variable
When using a switch
statement, if none of the cases are matched, a default
block can be used, as in Listing 4.36.
Listing 4.36 Using a default
Case with a switch
Statement
When there is a need to match more than one condition, you can use fallthrough
to allow more than one case to be matched. (See Listing 4.37.)
Listing 4.37 Using fallthrough
with a switch
Statement
In this chapter, we explored maps in Go. We explained how to declare and use maps and how to use control structures such as if
and switch
statements. We discussed that maps need to be initialized before they can be used. We showed you how to check for key existence, how to delete a key, and how to iterate over a map.
With this chapter, we have now covered most of the basic data types, operators, keywords, functions, and control structures in Go. With this knowledge, you can now begin to delve into more interesting, and fun, topics. If at any point in the rest of this book you start to feel lost, the answers are most likely in Chapters 1 through 4.