28. Many structs together
Chapters 26 and 27 built one value at a time. Structs are at their most useful with many values of one kind treated as a group: a party of characters, a swarm of enemies. This chapter combines two things you know, a struct with methods and a slice, into the everyday pattern of a slice of structs.
A type to make many of
A small critter type, with two methods. It follows the
rules from Chapter 27:
type critter struct {
name string
energy int
}
func (c *critter) play() {
c.energy = c.energy - 1
fmt.Printf("%s plays. Energy: %d\n", c.name, c.energy)
}
func (c *critter) isTired() bool {
return c.energy <= 0
}Put the structs in a slice
A slice can hold any one type, and that includes your own. Here is a
[]critter:
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
{name: "Nova", energy: 1},
}
fmt.Println(critters) // [{Spark 3} {Bolt 2} {Nova 1}]Inside the literal, Go lets you leave out the type in front of each
{. critters is a normal slice from Chapter 22.
What is new is that each item has its own fields and methods, which you
reach through its position:
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
}
critters[1].energy = 10
critters[1].play() // Bolt plays. Energy: 9
fmt.Println(critters[0].energy) // 3Loop and call a method on each
The obvious loop looks like it works:
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
}
for _, c := range critters {
c.play()
}Output:
Spark plays. Energy: 2
Bolt plays. Energy: 1
The numbers look right. Now print the slice afterwards, as
exercises/28/02-range-copy.go does:
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
}
for _, c := range critters {
c.play()
}
fmt.Println(critters) // [{Spark 3} {Bolt 2}]range gives you a copy of each struct.
Chapter 22 warned about this for ints. In the loop above, c
is a variable holding a copy of the item. c.play() takes
the location of that copy, and changes it. The critters in the slice
never lose any energy.
It compiles, it runs, and the printed numbers look right on the first
round. Nothing tells you that anything is wrong until the second loop
prints Energy: 2 again. Taking &c inside
the loop does not help either, because that is a pointer to the
copy.
There are two fixes. The first is to use the position, as Chapter 22 did:
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
}
for i := range critters {
critters[i].play()
}
fmt.Println(critters) // [{Spark 2} {Bolt 1}]critters[i] is the item itself, not a copy. One loop,
and each critter tracks its own energy. This is the core move of this
chapter: keep a collection, act on it as a group, and let each value
track itself.
Open exercises/28/01-critters.go. It makes each critter
play once with a position loop, then counts the tired ones. Add a second
play loop before the count, and watch the energies drop. Then write the
loop as for _, c := range critters, and see what the count
says.
A slice of pointers
The second fix is to keep pointers in the slice. The type is
[]*critter. Now range copies a pointer, and
the copy leads to the same critter:
// Chapter 28 example: a slice of pointers. range copies the pointer, and the
// copy still points at the same critter.
// Run with: go run exercises/28/03-pointer-slice.go
package main
import "fmt"
type critter struct {
name string
energy int
}
func newCritter(name string, energy int) *critter {
return &critter{name: name, energy: energy}
}
func (c *critter) play() {
c.energy = c.energy - 1
fmt.Printf("%s plays. Energy: %d\n", c.name, c.energy)
}
func main() {
critters := []*critter{
newCritter("Spark", 3),
newCritter("Bolt", 2),
}
for _, c := range critters {
c.play()
}
for _, c := range critters {
c.play()
}
fmt.Println(critters[0].energy) // 1
fmt.Println(critters[1].energy) // 0
}Output:
Spark plays. Energy: 2
Bolt plays. Energy: 1
Spark plays. Energy: 1
Bolt plays. Energy: 0
1
0
c.play() now changes the real critter, so the plain
range loop is right. A second name for the same critter
also stays in step: first := critters[0] shares it, where
with a []critter it would be a copy.
Which one to choose? Both are common:
- Use
[]critterwith a position loop for small values, when each one lives in this one slice. - Use
[]*critterwhen the values change often, or when something else also needs to hold on to the same value.
In a []*critter an item can be nil, and
calling play on it would crash, as Chapter 27 showed. Fill
the slice with real critters.
Filtering and counting
It is still a slice, so every loop pattern from Chapter 20 works. Count how many critters are tired:
critters := []critter{
{name: "Spark", energy: 2},
{name: "Bolt", energy: 1},
{name: "Nova", energy: 0},
}
tired := 0
for i := range critters {
if critters[i].isTired() {
tired = tired + 1
}
}
fmt.Println("Tired critters:", tired) // Tired critters: 1The if calls a method instead of testing a plain
variable, and the count pattern is unchanged.
When a loop only reads, the copy from range
does no harm, and the short form is fine if you read the fields
directly:
critters := []critter{
{name: "Spark", energy: 2},
{name: "Bolt", energy: 1},
}
total := 0
for _, c := range critters {
total = total + c.energy
}
fmt.Println(total) // 3Adding and removing
append and slicing from Chapter 22 work on a slice of
structs just as on a slice of numbers:
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
}
critters = append(critters, critter{name: "Zip", energy: 5}) // a new critter joins
critters = critters[1:] // the first one leaves
fmt.Println(critters) // [{Bolt 2} {Zip 5}]Sorting
slices.SortFunc from Chapter 24 takes a function that
compares two items. For structs, the function compares whichever field
you want to sort by. Here is exercises/28/04-sort.go:
// Chapter 28 example: sorting a slice of structs with a named function.
// Run with: go run exercises/28/04-sort.go
package main
import (
"fmt"
"slices"
"strings"
)
type critter struct {
name string
energy int
}
func byEnergy(a critter, b critter) int {
return a.energy - b.energy
}
func byName(a critter, b critter) int {
return strings.Compare(a.name, b.name)
}
func main() {
critters := []critter{
{name: "Spark", energy: 3},
{name: "Bolt", energy: 2},
{name: "Nova", energy: 1},
}
slices.SortFunc(critters, byEnergy)
fmt.Println(critters) // [{Nova 1} {Bolt 2} {Spark 3}]
slices.SortFunc(critters, byName)
fmt.Println(critters) // [{Bolt 2} {Nova 1} {Spark 3}]
}byEnergy returns a negative number if a
goes first, as Chapter 24 said. strings.Compare gives the
same kind of answer for two strings. For a []*critter, the
function's parameters are *critter instead.
Maps of structs
A map can hold structs too, and reading a field is simple:
team["spark"].energy. Assigning to one is not. This is
exercises/28/06-map-field-error.go, inside
main:
team := map[string]critter{
"spark": {name: "Spark", energy: 3},
}
team["spark"].energy = 2
fmt.Println(team["spark"].energy)Running it stops with:
# command-line-arguments
exercises\28\06-map-field-error.go:21:2: cannot assign to struct field team["spark"].energy in map
A map hands you a copy of what it holds, and Go refuses to let you change one field of a copy that is about to vanish. For the same reason, you cannot call a pointer method on an item in the map:
team := map[string]critter{
"spark": {name: "Spark", energy: 3},
}
team["spark"].play()cannot call pointer method play on critter
There are two fixes, in
exercises/28/05-map-of-structs.go. The first is to copy the
struct out, change the copy, and put it back:
team := map[string]critter{
"spark": {name: "Spark", energy: 3},
}
c := team["spark"]
c.energy = 2
team["spark"] = c
fmt.Println(team["spark"].energy) // 2The second is to keep pointers in the map. Assigning through a pointer does not change what the map holds, so Go allows it:
pets := map[string]*critter{
"bolt": {name: "Bolt", energy: 2},
}
pets["bolt"].energy = 1
fmt.Println(pets["bolt"].energy) // 1A missing key in a map of pointers gives the zero value, which is
nil, so pets["nova"].energy crashes. Check
with the v, ok form from Chapter 23 first:
pets := map[string]*critter{
"bolt": {name: "Bolt", energy: 2},
}
pet, ok := pets["nova"]
if !ok {
fmt.Println("No critter called nova.") // No critter called nova.
return
}
fmt.Println(pet.energy)As always, range over a map has no order. To print a map
of structs, sort the keys first, as in Chapter 23.
Homework
Homework starters are in exercises/28/homework/. Each
one runs as it is and prints a placeholder line. All four have the same
small hero struct and newHero function at the
top.
Problem 1 — A party of heroes
Open exercises/28/homework/01-party.go. Make a slice of
three heroes with different names and hit points, then loop over it and
print each one's name and HP on its own line.
Problem 2 — Total HP
Open exercises/28/homework/02-total-hp.go. Use the
accumulate pattern to add up everyone's HP, and print the team
total.
Problem 3 — Who is hurt?
Open exercises/28/homework/03-who-is-hurt.go. Each hero
has an hp and a maxHP. Lower one hero's
hp through the slice position, then loop over the party and
print every hero whose hp is below maxHP.
Challenge — Strongest hero
Open exercises/28/homework/04-strongest.go. Loop over
the party and print the name of the hero with the most HP. Use the
accumulate-a-maximum idea: remember the best so far, and replace it when
you find a stronger one.
Stuck or finished? Open the homework solutions page.