Automate the Boring Stuff with Go, Ch. 6: Maps and Structs
Part of Automate the Boring Stuff with Go, a beginner-friendly guide to programming in Go.
Updated for Go 1.27 in October 2026. What changed: this used to be the shortest chapter in the book. It now covers maps properly (comma-ok, delete, iteration order, sorted keys with the maps package) and introduces structs, which the first edition never did.
Like a map of New York City, a map in Go is a thing you look something up in. You give it a key, it gives you the value filed under that key. Where a slice is a list you walk by position, a map is a lookup table you ask by name.
Structs are the other half of this chapter: a way to group a few named values into one thing, so “a city” can travel through your program as one variable instead of three. The code is in the repo under ch6/examples.
Maps
Syntax
A map type is written map[KeyType]ValueType. Here both maps have string keys and int values:
// ex. 1 a map: keys of one type, values of another
package main
import "fmt"
func main() {
nyc := map[string]int{
"population": 8000000,
"size": 302,
}
htx := map[string]int{
"population": 2000000,
"size": 665,
}
fmt.Println(htx["population"], nyc["population"])
}
Looking a value up uses square brackets, like a slice, but with the key instead of a position.
Adding, updating, testing, deleting
// ex. 2 add, look up, test for presence, delete
package main
import "fmt"
func main() {
stock := map[string]int{"apples": 12, "pears": 3}
stock["plums"] = 20 // add
stock["pears"]++ // update
// a missing key gives the zero value, which is why the comma-ok form exists
fmt.Println(stock["bananas"])
n, ok := stock["bananas"]
fmt.Println(n, ok)
delete(stock, "apples")
fmt.Println(len(stock), stock)
}
Assigning to a key that does not exist adds it. Reading a key that does not exist does not fail: you get the zero value for the value type, 0 for an int, "" for a string. That is convenient right up until you need to know whether the key was there, which is what the second form, n, ok := stock["bananas"], is for. ok is false when the key is missing. delete removes a key; len counts them.
Iteration order is random
You can range over a map, and you get key and value instead of index and value. But the order is deliberately random, different on every run, so that nobody accidentally depends on it. When order matters, sort the keys:
// ex. 3 iteration order is random, so sort the keys when order matters
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ages := map[string]int{"Zed": 41, "Ada": 36, "Mo": 29}
for name, age := range ages { // a different order every run
fmt.Println(name, age)
}
for _, name := range slices.Sorted(maps.Keys(ages)) { // always alphabetical
fmt.Println(name, ages[name])
}
}
maps.Keys gives you the keys and slices.Sorted turns them into a sorted slice. You will type that pair a lot.
Counting things
The zero-value behaviour makes maps the natural way to count:
// ex. 5 a map of structs, keyed by name
package main
import (
"fmt"
"maps"
"slices"
)
type City struct {
Population int
SizeSqMi float64
}
func main() {
cities := map[string]City{
"New York": {Population: 8000000, SizeSqMi: 302},
"Houston": {Population: 2000000, SizeSqMi: 665},
"Austin": {Population: 980000, SizeSqMi: 326},
}
for _, name := range slices.Sorted(maps.Keys(cities)) {
c := cities[name]
fmt.Printf("%-9s %8d people, %.0f/sq mi\n", name, c.Population, float64(c.Population)/c.SizeSqMi)
}
}
counts[word]++ works on the first sighting of a word because a missing key reads as 0.
Structs
A map holds many values of the same type under different keys. When one thing has a few fields of different types, a name, a population, a size in square miles, you want a struct:
// ex. 4 a struct groups named fields of different types
package main
import "fmt"
// City is a type we define. Capitalised field names are visible to other packages.
type City struct {
Name string
Population int
SizeSqMi float64
}
func (c City) Density() float64 {
return float64(c.Population) / c.SizeSqMi
}
func main() {
nyc := City{Name: "New York", Population: 8000000, SizeSqMi: 302}
htx := City{Name: "Houston", Population: 2000000, SizeSqMi: 665}
fmt.Printf("%s: %.0f people per square mile\n", nyc.Name, nyc.Density())
fmt.Printf("%+v\n", htx) // %+v prints field names too
}
type City struct { ... } defines a new type with three named fields. City{Name: ..., Population: ...} builds one; nyc.Name reads a field. The func (c City) Density() line attaches a method to the type, a function you call as nyc.Density(). Field and method names that start with a capital letter are visible outside the package, which matters in chapter 12 when a struct becomes JSON.
%+v in a Printf prints a struct with its field names, which is handy while debugging.
Maps of structs
Put the two together and you have a small database:
// ex. 6 counting things is what maps are for
package main
import (
"fmt"
"strings"
)
func main() {
text := "the cat sat on the mat and the cat slept"
counts := map[string]int{}
for _, word := range strings.Fields(text) {
counts[word]++ // a missing key starts at 0, so this just works
}
fmt.Println(counts)
fmt.Println("the:", counts["the"])
}
Slices of structs
And a slice of structs is a table, one struct per row. Most of the spreadsheet work in chapter 9 is turning rows into a slice of structs and back:
// ex. 7 a slice of structs is a table
package main
import (
"fmt"
"slices"
)
type Expense struct {
Day string
Item string
Amount float64
}
func main() {
expenses := []Expense{
{"Mon", "coffee", 4.50},
{"Mon", "lunch", 12.00},
{"Tue", "coffee", 4.50},
{"Tue", "book", 28.99},
}
total := 0.0
byDay := map[string]float64{}
for _, e := range expenses {
total += e.Amount
byDay[e.Day] += e.Amount
}
fmt.Printf("total %.2f\n", total)
for _, day := range []string{"Mon", "Tue"} {
fmt.Printf("%s %.2f\n", day, byDay[day])
}
// sort the table by amount, biggest first
slices.SortFunc(expenses, func(a, b Expense) int {
switch {
case a.Amount > b.Amount:
return -1
case a.Amount < b.Amount:
return 1
}
return 0
})
fmt.Println(expenses[0])
}
slices.SortFunc sorts by whatever rule you give it; the function returns negative, zero or positive, the way a comparison does.
When to use which
- A slice when you have many of the same thing and position matters (a list of prices, lines of a file).
- A map when you look things up by a key (word counts, settings by name, a city by its name).
- A struct when one thing has several named parts (a city, a row, a configuration).
- A slice of structs for a table, a map of structs for a table you look up by key.
Conclusion
That completes the core of the language for this book: types, control flow, functions, slices, maps and structs. The curious reader should look at Go Maps in Action by the Go team for more on maps. Now let’s move on to part II, where we put all of it to work automating the boring stuff.