Maps
You can find all the code for this chapter here
In arrays & slices, you saw how to store values in order. Now, we will look at a way to store items by a key and look them up quickly.
Maps allow you to store items in a manner similar to a dictionary. You can think of the key as the word and the value as the definition. And what better way is there to learn about Maps than to build our own dictionary?
First, assuming we already some words with their definitions in the dictionary, if we search for a word, it should return the definition of it.
Write the test first
In dictionary_test.go
package main
import "testing"
func TestSearch(t *testing.T) {
dictionary := map[string]string{"test": "this is just a test"}
got := Search(dictionary, "test")
want := "this is just a test"
if got != want {
t.Errorf("got '%s' want '%s' given, '%s'", got, want, "test")
}
}Declaring a Map is somewhat similar to an array. Except, it starts with the map keyword and requires two types. The first is the key type, which is written inside the []. The second is the value type, which goes right after the [].
The key type is special. It can only be a comparable type because without the ability to tell if 2 keys are equal, we have no way to ensure that we are getting the correct value. Comparable types are explained in depth in the language spec.
The value type, on the other hand, can be any type you want. It can even be another map.
Everything else in this test should be familiar.
Try to run the test
By running go test the compiler will fail with ./dictionary_test.go:8:9: undefined: Search.
Write the minimal amount of code for the test to run and check the output
In dictionary.go
Your test should now fail with a clear error message
dictionary_test.go:12: got '' want 'this is just a test' given, 'test'.
Write enough code to make it pass
Getting a value out of a Map is the same as getting a value out of Array map[key].
Refactor
I decided to create an assertStrings helper and get rid of the given piece to make the implementation more general.
Using a custom type
We can improve our dictionary's usage by creating a new type around map and making Search a method.
In dictionary_test.go:
We started using the Dictionary type, which we have not defined yet. Then called Search on the Dictionary instance.
We did not need to change assertStrings.
In dictionary.go:
Here we created a Dictionary type which acts as a thin wrapper around map. With the custom type defined, we can create the Search method.
Write the test first
The basic search was very easy to implement, but what will happen if we supply a word that's not in our dictionary?
We actually get nothing back. This is good because the program can continue to run, but there is a better approach. The function can report that the word is not in the dictionary. This way, the user isn't left wondering if the word doesn't exist or if there is just no definition (this might not seem very useful for a dictionary. However, it's a scenario that could be key in other usecases).
The way to handle this scenario in Go is to return a second argument which is an Error type.
Errors can be converted to a string with the .Error() method, which we do when passing it to the assertion. We are also protecting assertStrings with if to ensure we don't call .Error() on nil.
Try and run the test
This does not compile
Write the minimal amount of code for the test to run and check the output
Your test should now fail with a much clearer error message.
dictionary_test.go:22: expected to get an error.
Write enough code to make it pass
In order to make this pass, we are using an interesting property of the map lookup. It can return 2 values. The second value is a boolean which indicates if the key was found successfully.
This property allows us to differentiate between a word that doesn't exist and a word that just doesn't have a definition.
Refactor
We can get rid of the magic error in our Search function by extracting it into a variable. This will also allow us to have a better test.
By creating a new helper we were able to simplify our test, and start using our ErrNotFound variable so our test doesn't fail if we change the error text in the future.
Write the test first
We have a great way to search the dictionary. However, we have no way to add new words to our dictionary.
In this test, we are utilizing our Search function to make the validation of the dictionary a little easier.
Write the minimal amount of code for the test to run and check output
In dictionary.go
Your test should now fail
Write enough code to make it pass
Adding to a map is also similar to an array. You just need to specify a key and set it equal to a value.
Reference Types
An interesting property of maps is that you can modify them without passing them as a pointer. This is because map is a reference type. Meaning it holds a reference to the underlying data structure, much like a pointer. The underlying data structure is a hash table, or hash map, and you can read more about hash tables here.
Maps being a reference is really good, because no matter how big a map gets there will only be one copy.
A gotcha that reference types introduce is that maps can be a nil value. If you try to use a nil map, you will get a nil pointer exception which will stop your program's execution in its tracks.
Because of nil pointer exceptions, you should never initialize an empty map variable:
Instead, you can initialize an empty map like we were doing above, or use the make keyword to create a map for you:
Both approaches create an empty hash map and point dictionary at it. Which ensures that you will never get a nil pointer exception.
Refactor
There isn't much to refactor in our implementation but the test could use a little simplification.
We made variables for word and definition, and moved the definition assertion into its own helper function.
Our Add is looking good. Except, we didn't consider what happens when the value we are trying to add already exists!
Map will not throw an error if the value already exists. Instead, they will go ahead and overwrite the value with the newly provided value. This can be convenient in practice, but makes our function name less than accurate. Add should not modify existing values. It should only add new words to our dictionary.
Write the test first
For this test, we modified Add to return an error, which we are validating against a new error variable, ErrWordExists. We also modified the previous test to check for a nil error.
Try to run test
The compiler will fail because we are not returning a value for Add.
Write the minimal amount of code for the test to run and check the output
In dictionary.go
Now we get two more errors. We are still modifying the value, and returning a nil error.
Write enough code to make it pass
Here we are using a switch statement to match on the error. Having a switch like this provides an extra safety net, in case Search returns an error other than ErrNotFound.
Refactor
We don't have too much to refactor, but as our error usage grows we can make a few modifications.
We made the errors constant; this required us to create our own DictionaryErr type which implements the error interface. You can read more about the details in this excellent article by Dave Cheney. Simply put, it makes the errors more reusable and immutable.
Next, let's create a function to Update the definition of a word.
Write the test first
Update is very closely related to Add and will be our next implementation.
Try and run the test
Write minimal amount of code for the test to run and check the failing test output
We already know how to deal with an error like this. We need to define our function.
With that in place, we are able to see that we need to change the definition of the word.
Write enough code to make it pass
We already saw how to do this when we fixed the issue with Add. So let's implement something really similar to Add.
There is no refactoring we need to do on this since it was a simple change. However, we now have the same issue as with Add. If we pass in a new word, Update will add it to the dictionary.
Write the test first
We added yet another error type for when the word does not exist. We also modified Update to return an error value.
Try and run the test
We get 3 errors this time, but we know how to deal with these.
Write the minimal amount of code for the test to run and check the failing test output
We added our own error type and are returning a nil error.
With these changes, we now get a very clear error:
Write enough code to make it pass
This function looks almost identical to Add except we switched when we update the dictionary and when we return an error.
Note on declaring a new error for Update
We could reuse ErrNotFound and not add a new error. However, it is often better to have a precise error for when an update fails.
Having specific errors gives you more information about what went wrong. Here is an example in a web app:
You can redirect the user when
ErrNotFoundis encountered, but display an error message whenErrWordDoesNotExistis encountered.
Next, let's create a function to Delete a word in the dictionary.
Write the test first
Our test creates a Dictionary with a word and then checks if the word has been removed.
Try to run the test
By running go test we get:
Write the minimal amount of code for the test to run and check the failing test output
After we add this, the test tells us we are not deleting the word.
Write enough code to make it pass
Go has a built-in function delete that works on maps. It takes two arguments. The first is the map and the second is the key to be removed.
The delete function returns nothing, and we based our Delete method on the same notion. Since deleting a value that's not there has no effect, unlike our Update and Add** methods, we don't need to complicate the API with errors.
Wrapping up
In this section, we covered a lot. We made a full CRUD (Create, Read, Update and Delete) API for our dictionary. Throughout the process we learned how to:
Create maps
Search for items in maps
Add new items to maps
Update items in maps
Delete items from a map
Learned more about errors
How to create errors that are constants
Writing error wrappers
Last updated
Was this helpful?