t0ul

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.

← Back to the blog