3 A bookworm’s digest: playing with loops and maps
This chapter uses a small, book-themed CLI project to deepen core Go skills while keeping the scope approachable. From a shared JSON file of bookworms and their shelves, you build a tool that loads data, finds books read by more than one person, and cleanly prints the results. Along the way, you reinforce command-line basics, practice working with slices and maps, and learn to decode JSON, all under simple constraints (each book has a single author and the input file remains small). A stretch goal introduces simple recommendation ideas based on overlapping reading tastes.
The implementation walks through practical, idiomatic patterns: opening files with os.Open and deferring Close; decoding JSON into exported struct fields with json tags; and validating behavior with table-driven tests and small test helpers. It highlights Go’s single for keyword for all loop forms, the range idiom over slices and maps, and key map concepts (zero values, presence checks with the “comma ok” form, and using comparable types as keys). You count occurrences with a map[Book]uint to identify common titles, address nondeterministic map iteration by sorting the final slice, and compare two sorting approaches—sort.Slice with an inline comparator and implementing sort.Interface for reusable, custom ordering.
Finally, you connect the pieces in main to print the common books, then explore focused improvements: a concise display function; stable, test-friendly output via sorting; a lightweight recommendation strategy that leverages set-like maps (map[Book]struct{}) to suggest related reads; and buffered I/O with bufio to tune system calls when reading or writing larger files. Along the way, the chapter clarifies how slices differ from arrays, how make and append affect length and capacity, and why determinism and small, composable helpers (for counting, sorting, and equality checks) make code easier to test, reason about, and evolve.
Summary
- The JSON format is commonly used when representing structured data. It can hold slices, maps, and objects.
- The encoding/json package exposes two ways of decoding a JSON message: the json.Decoder type, and the json.Unmarshal function. Most of the time, the json.Decoder should be used, as it can read from an io.Reader rather than from a complete slice of bytes. The encoding/json package also offers similar functions to encode a JSON message: json.Encoder, and json.Marshal.
- Fields of Go structures must be exported if something has to be decoded into them, so the decoder can access them.
- Test data should live in a folder named testdata, which will be automatically ignored by the go tool when compiling the code.
- All loops are for loop, the keyword can be followed by a variety of different syntaxes. The most common are for i := min; i < limit; i++ { and for condition() {.
- To loop over values stored in a slice or an array, use the for index, value := range mySlice { syntax. The index will start at 0 and its last value will be len(values)-1. If you don’t really need the index, then you can iterate over the values with for _, value := mySlice values {. From time to time, you will see the following syntax: for index := range mySlice {. In this case, only the index is retrieved. This can sometimes be useful, as it doesn’t cause each value of the slice to be copied.
- To loop over all pairs of keys and values stored in a map, we again use the for keyword: for key, value := range myMap {. If you are only interested in values, you can, just like we did with the slice, use for _, value := range myMap {. If, on the other hand, you are only interested in the keys of the map, then it’s even shorter: for key := range map {.
- Slices are an abstraction of arrays. When in doubt, use slices.
- Sort slices with the sort package, either by giving a sorting function as a parameter to sort.Slice or by implementing the package’s Interface, which consists of a Len function, returning the length of the slice, a Swap function, which permutes two entries, and a Less function, that returns the comparison of two entries.
- A map’s key can be an int, a string, or even a struct. When it’s a structure, the structure can’t contain a pointer, a slice, a map, a channel, a function, or a field of type interface{}. Indeed, these types cannot be compared with other values.
- If you want to discard repeated values from a slice - for instance, you want to have the list of bands that appear in your collection of CDs - you can use a map[X]struct{}. Iterate through your list and write into the map the elements you want to keep. In the music example, we’d write myMap[cd.Band] = struct{}{}. At the end of the loop, the keys of the map will be the unique values you had in your original list.
- When you want to know whether an element appears in a slice, and you know you’ll do the operation so many times that you can’t afford to iterate through the slice every time, you can use map[X]bool. Start by adding keys to this map by iterating over your list once, and set their value to true. After that, you can check whether an element is in the map by running found := myMap[element]. If it was initially in the slice, then the value returned is true - what we set it to -, and if the element wasn't in the slice, the value returned is the zero-value of a boolean - false.
- Remember to call Close() when you Open or Create a file. The defer keyword is your friend here.
- Use defer to keep together in the code lines that make sense together but need to be executed at different moments.
- Using a bufio.Reader or a bufio.Writer will reduce the amount of system calls your program executes. It will also make it simpler to count such system calls.
- When using a bufio.Writer, always keep in mind that the buffer needs to be Flushed before the data can be considered fully written.
FAQ
How do I load the bookworms from a JSON file in Go?
Open the file, defer its Close, then decode into exported structs using encoding/json. Example:f, err := os.Open(path); if err != nil { return ... } defer f.Close(); var bws []Bookworm; if err := json.NewDecoder(f).Decode(&bws); err != nil { return ... } return bws, nil.My JSON decoding yields empty fields. What am I doing wrong?
Ensure struct fields are exported (start with a capital letter) and tagged with the correct JSON field names. Example:type Book struct { Author string `json:"author"`; Title string `json:"title"` }. Unexported fields are invisible to the decoder.What’s the difference between os.Open, os.Create, and os.OpenFile?
- os.Open: read-only open; does not create; returns *os.File.
- os.Create: creates/truncates file with 0666 perms; read-write; beware of data loss if file exists.
- os.OpenFile: fully configurable; combine flags like
os.O_APPEND|os.O_CREATE|os.O_WRONLYand pass custom perms.
*os.File or *os.PathError on failure.Why should I use defer Close() right after opening a file?
defer ensures the file is closed when the function returns—on every path, even early returns or panics—releasing OS resources and avoiding leaks/locks. Deferred calls run in LIFO order.How do I count occurrences with a map, and why does count[key]++ work without initialization?
Maps return the zero value for missing keys. Withmap[Book]uint, a missing key yields 0, so count[b]++ expands to “read 0, add 1, write back.” Initialize the map once with count := make(map[Book]uint).Can I use a struct as a map key?
Yes, if the type is comparable. Structs composed only of comparable fields (e.g., strings) are fine. Types like slices, maps, or functions are not comparable, and any struct containing them cannot be a map key.How does range work on slices and maps?
- Slices:
for i, v := range syields index and a copy of the element (vchanges don’t affects[i]). - Maps:
for k, v := range myields copies of key and value; iteration order is intentionally random/non-deterministic. - Ignore unused parts via
_:for _, v := range sorfor k := range m.
Why does my output order change between runs, and how can I make it deterministic?
Map iteration is unordered. Collect results into a slice and sort it. Example with sort.Slice:sort.Slice(books, func(i, j int) bool { if books[i].Author != books[j].Author { return books[i].Author < books[j].Author } return books[i].Title < books[j].Title }).What’s the difference between arrays and slices, and when should I use make/append?
Arrays ([n]T) have fixed length and are rarely used directly. Slices ([]T) are dynamic views over arrays with length and capacity. Use make([]T, len, cap) to preallocate and append to grow: var xs []T; xs = append(xs, v) or xs := make([]T, 0, n) to avoid reallocations.How should I structure tests for these functions?
Use table-driven tests with descriptive case names and loop via range. Compare results with small helpers (e.g.,equalBooks, equalBooksCount) or reflect.DeepEqual when performance isn’t critical. For CLI output, add an Example test. Remember order sensitivity—sort results before asserting.
Learn Go with Pocket-Sized Projects ebook for free