You can find all the code for this chapter here
In the previous chapter, 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?
In dict_test.go
package main
import "testing"
func TestSearch(t *testing.T) {
dict := map[string]string{"test": "this is just a test"}
got := Search(dict, "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, which is written
inside the []
. The second is the value, which goes right after the []
.
The key is special. It can only be a comparable type. Comparable types are explained in depth in the language spec. But the simple version is:
- boolean
- numeric
- string
- pointer
- channel
- interface types
- structs that contain comparable types
- arrays that contain comparable types
if you don't know what some of these are yet, don't worry. We will get to them later in the book.
The value, on the other hand, can be any type you want. It can even be another Map.
Everything else in this test should be familiar.
By running go test
the compiler will fail with ./dict_test.go:8:9: undefined: Search
.
In dict.go
package main
func Search(dict map[string]string, word string) string {
return ""
}
Your test should now fail with a clear error message
dict_test.go:12: got want this is just a test given, test
func Search(dict map[string]string, word string) string {
return dict[word]
}
Getting a value our of a Map is the same as getting a value out of Array
map[key]
.
Our test output wasn't very clear. Let's make a small change to increase readability and extract our assertion.
func TestSearch(t *testing.T) {
dict := map[string]string{"test": "this is just a test"}
got := Search(dict, "test")
want := "this is just a test"
assertStrings(t, got, want)
}
func assertStrings(t *testing.T, got, want string) {
t.Helper()
if got != want {
t.Errorf("got '%s' want '%s'", got, want)
}
}
With this in place our failing test looks a lot clearer
dict_test.go:12: got '' want 'this is just a test'
.
I also decided to get rid of the given piece. That way this assertion is more generally useful.
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).
func TestSearch(t *testing.T) {
dict := map[string]string{"test": "this is just a test"}
t.Run("known word", func(t *testing.T) {
got, _ := Search(dict, "test")
want := "this is just a test"
assertStrings(t, got, want)
})
t.Run("unknown word", func(t *testing.T) {
_, got := Search(dict, "test")
want := "could not find the word you were looking for"
if got == nil {
t.Error("expected to receive and error.")
} else {
assertStrings(t, got.Error(), want)
}
})
}
The way to handle this scenario in Go is to return a second argument which is
an Error
type.
Error
s can be converted to a string with the .Error()
method, which we do when passing it to the assertion. We are also wrapping
assertString
in an if to ensure we don't call .Error()
on nil
.
This does not compile
./dict_test.go:18:10: assignment mismatch: 2 variables but 1 values
func Search(dict map[string]string, word string) (string, error) {
return dict[word], nil
}
Your test should now fails with a much clearer error message.
dict_test.go:22: expected to receive and error.
func Search(dict map[string]string, word string) (string, error) {
def, ok := dict[word]
if !ok {
return "", errors.New("could not find the word you were looking for")
}
return def, nil
}
In order to make this pass we are using an interesting property of the Map lookup. It can return 2 values. The second value being 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.
var NotFoundError = errors.New("could not find the word you were looking for")
func Search(dict map[string]string, word string) (string, error) {
def, ok := dict[word]
if !ok {
return "", NotFoundError
}
return def, nil
}
We can get rid of the magic error in our Search
function by bringing it up
into a constant. This will also allow us to have a better test.
t.Run("unknown word", func(t *testing.T) {
_, got := Search(dict, "unknown")
assertError(t, got, NotFoundError)
})
func assertError(t *testing.T, got, want error) {
t.Helper()
if got != want {
t.Errorf("got error '%s' want '%s'", got, want)
}
}
By creating a new helper we were able to simplify our test, and start using
our NotFoundError
variable so our test doesn't fail if we change the error
text in the future.