4. Maps and Control Structures

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

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

Length and Capacity

You can use built-in len1 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

Initializing Maps

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 make2 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

Uninitialized Maps

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

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

Structs as Keys

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

Iterating Maps

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

Deleting Keys from a Map

You can use the built-in delete3 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

Nonexistent Map Keys

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

Checking Map Key Existence

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

Exploiting Zero Value

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

Testing Presence Only

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

Maps and Complex Values

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

Copy on Insert

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

Updating Complex Map Values

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

Listing Keys 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.

Sorting Keys

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 sort4 package, Listing 4.24, provides a number of functions and interfaces for sorting collection types.

4. https://pkg.go.dev/sort

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.Ints5 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

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

The else Statement

We 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

The else if Statement

When 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.

Assignment Scope

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

Logic and Math Operators

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 Statements

Switch 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

Default

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

Fallthrough

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

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset