26. Structs and methods
So far, values that belong together have lived apart: a quest's
reward in one map, its steps in another; a player's room and inventory
in two variables outside main. This chapter introduces the
struct, which keeps related values in one place, and
the method, a function that belongs to a type. Part 6
is built on these two ideas.
Keeping related data together
Chapter 23 ended with a quest stored in two maps, and said that nothing keeps them in step. It also said that one map has one value type, so a name and a reward cannot share a map. A struct solves both.
A struct is a value made of named
fields, and each field has its own type. You define it
once with type, at the top level of the file, next to your
functions:
type quest struct {
name string
reward int
steps []string
}
func main() {
errand := quest{
name: "Healer's Errand",
reward: 50,
steps: []string{"Speak to the elder", "Collect 5 herbs", "Return to camp"},
}
fmt.Println(errand.reward) // 50
fmt.Println(errand.steps[0]) // Speak to the elder
fmt.Println(len(errand.steps)) // 3
}Read it as: define a new type called quest, which
has these three fields. The value in main is a
struct literal: the type, then
field: value pairs in curly braces. As with a map on
several lines, every line needs a comma after it, even the last one.
You read a field with a dot, and you assign to one the same way:
errand := quest{name: "Healer's Errand", reward: 50}
errand.reward = 60
fmt.Println(errand.reward) // 60This is the dot notation that maps do not have, as Chapter 23 showed. Go checks the field names before the program runs, so a misspelt name stops it. With a map, the same mistake would quietly give you a zero:
errand := quest{name: "Healer's Errand", rewrd: 50}
fmt.Println(errand)unknown field rewrd in struct literal of type quest
Zero values
A field you leave out of the literal gets the zero
value of its type: 0 for an int, ""
for a string, false for a bool, and nil for a
slice or a map. Writing the type with empty braces gives a struct of
zero values:
blank := quest{name: "Unwritten"}
fmt.Printf("%q %d %d\n", blank.name, blank.reward, len(blank.steps)) // "Unwritten" 0 0
empty := quest{}
fmt.Printf("%q\n", empty.name) // ""To print a whole struct, %v gives the values, and
%+v adds the field names:
errand := quest{name: "Lost Cat", reward: 5}
fmt.Printf("%v\n", errand) // {Lost Cat 5 []}
fmt.Printf("%+v\n", errand) // {name:Lost Cat reward:5 steps:[]}Go also lets you leave out the field names, as in
quest{"Lost Cat", 5, nil}. The values must then be all
there, and in order. Adding a field later breaks every such literal, so
this book always writes the names.
One map instead of two
With a struct, the two maps from Chapter 23 become one. The key finds the quest, and the quest holds everything about itself:
quests := map[string]quest{
"Healer's Errand": {
name: "Healer's Errand",
reward: 50,
steps: []string{"Speak to the elder", "Collect 5 herbs", "Return to camp"},
},
"Lost Cat": {name: "Lost Cat", reward: 5, steps: []string{"Find the cat"}},
}
fmt.Println(quests["Lost Cat"].reward) // 5
fmt.Println(len(quests["Healer's Errand"].steps)) // 3The two maps can no longer drift out of step, because there is only
one. A quest cannot have a reward and lack an entry for its steps.
Inside the literal, Go lets you leave out the inner type, as it does for
nested maps. These examples are in
exercises/26/01-quest.go.
One room in one value
The Text Adventure kept a room in four maps, because a name, a
description, exits and items are different types. A struct puts one room
in one value. Here is the main part of
exercises/26/02-room.go:
type room struct {
name string
description string
exits map[string]int
items []string
}
func main() {
rooms := map[int]room{
1: {
name: "Forest Path",
description: "You are on a moss covered forest path. Birdsong drifts on the air.",
exits: map[string]int{"north": 2},
},
2: {
name: "Clearing",
description: "A small clearing of wildflowers.",
exits: map[string]int{"south": 1, "east": 3},
items: []string{"torch"},
},
}
here := rooms[2]
fmt.Println("--", here.name, "--")
fmt.Println(here.description)
fmt.Println("Items here:", strings.Join(here.items, ", "))
fmt.Println("East leads to room", here.exits["east"])
}Output:
-- Clearing --
A small clearing of wildflowers.
Items here: torch
East leads to room 3
Everything about a room now travels together. To pass a room to a function, you pass one value, not an id and four maps.
Functions on structs, the long way
Chapter 21 kept a count between calls with a variable outside every function, and promised something cleaner. A struct can hold that count, and a function can take the struct and hand back the changed one:
type counter struct {
count int
}
func step(c counter) counter {
c.count = c.count + 1
return c
}
func main() {
c := counter{}
c = step(c)
c = step(c)
c = step(c)
fmt.Println(c.count) // 3
}Nothing lives outside the functions, and you can see from each call
what is read and what changes. You can also have two counters at once,
which one variable outside the functions could not do. It works, but
step is a general name, and every call passes the counter
explicitly.
Methods and the receiver
A method is a function that belongs to a type. Go has no classes: you define the type, and then write its methods as separate functions, next to it. What makes a function a method is one extra parameter, written in its own parentheses before the name:
type counter struct {
count int
}
func (c counter) show() {
fmt.Println("Count:", c.count)
}
func main() {
c := counter{count: 3}
c.show() // Count: 3
}That extra parameter, (c counter), is the
receiver. It is the value in front of the dot when the
method is called. In c.show(), Go hands the value of
c to show, and inside the body it is called
c, just as any parameter has a name. The receiver is
usually named with the first letter or two of the type.
Inside the body, the receiver is an ordinary variable:
c.count reads a field. Two values of a type each answer for
themselves, because each call receives its own value:
type counter struct {
count int
}
func (c counter) show() {
fmt.Println("Count:", c.count)
}
func main() {
a := counter{count: 1}
b := counter{count: 2}
a.show() // Count: 1
b.show() // Count: 2
}Methods also let different types use the same name: a
dog and a door can both have a method
open, because the type in front of the dot says which one
you mean.
A method gets a copy
The receiver in (c counter) is a value
receiver: the method gets a copy of the struct, the same way a
function gets a copy of an int parameter, as Chapter 21 said. So a
method like this cannot change the original:
type counter struct {
count int
}
func (c counter) stepWrong() {
c.count = c.count + 1
}
func main() {
c := counter{}
c.stepWrong()
c.stepWrong()
fmt.Println(c.count) // 0
}It compiles and runs, and the answer is 0. Each call
added 1 to its own copy, and the copy was gone when the call ended.
At this stage the way out is the one from the long version: give back
the changed copy, and assign it to the same variable. It is the same
habit as append in Chapter 22:
// Chapter 26 example: a counter, with a method that gives back a changed copy.
// Run with: go run exercises/26/03-counter.go
package main
import "fmt"
type counter struct {
count int
}
func (c counter) show() {
fmt.Println("Count:", c.count)
}
func (c counter) step() counter {
c.count = c.count + 1
return c
}
func main() {
c := counter{}
c = c.step()
c = c.step()
c = c.step()
c.show() // Count: 3
// Try this: add a method reset that gives back a counter with count set
// to 0, assign its result back to c, and call c.show() again.
}This prints Count: 3.
Open exercises/26/03-counter.go. Add a method
reset that gives back a counter with count set
to 0. Assign its result back to c, and call
c.show() again.
Assigning back works, but it is easy to forget. Go stops you if you
drop the result of append, but not the result of your own
method. c.step() on a line by itself compiles, and does
nothing to c. Chapter 27 removes this problem with a
pointer receiver, which lets a method change the
original.
Methods you have already called
Since Chapter 15 you have called
reader.ReadString('\n'). reader is a value,
and ReadString is a method on it. That is why it remembers
how far it has read from one call to the next.
Not everything with a dot is a method. In
strings.ToUpper(name), the part in front of the dot is a
package, and ToUpper is an ordinary function in it. The
string goes in as an argument. A string has no methods at all, so this
does not compile:
name := "keiko"
fmt.Println(name.ToUpper())name.ToUpper undefined (type string has no field or method ToUpper)
An error does have one. err.Error() gives
the message as a string. Here is the body of main in
exercises/26/04-method-or-function.go:
name := "keiko"
// A function from the strings package. The string goes in as an argument.
fmt.Println(strings.ToUpper(name)) // KEIKO
// A method. err is a value, and Error is a method you can call on it.
err := errors.New("out of arrows")
fmt.Println(err.Error()) // out of arrowsChapter 29 shows why fmt.Println(err) prints the same
message.
A small game value
Structs and methods are most useful when several pieces of state go together. Here is a dog with a name and a number of barks left:
// Chapter 26 example: a dog with state, a newDog function and a method.
// Run with: go run exercises/26/05-dog.go
package main
import "fmt"
type dog struct {
name string
barksLeft int
}
func newDog(name string, barksLeft int) dog {
return dog{name: name, barksLeft: barksLeft}
}
func (d dog) bark() dog {
if d.barksLeft <= 0 {
fmt.Printf("%s is hoarse.\n", d.name)
return d
}
fmt.Printf("%s: Woof!\n", d.name)
d.barksLeft = d.barksLeft - 1
return d
}
func main() {
rex := newDog("Rex", 3)
rex = rex.bark()
rex = rex.bark()
rex = rex.bark()
rex = rex.bark()
}Output:
Rex: Woof!
Rex: Woof!
Rex: Woof!
Rex is hoarse.
The struct holds the state, and the method works on it. A type with its data and the methods that act on it is the shape of the rest of this part.
Look at newDog. Go has no special function that runs
when you make a value. By convention, you write an ordinary function
called newThing that builds the value and returns it, as
bufio.NewReader does. Nothing in the language treats this
name specially. It is useful when making a value takes more than filling
in fields. Chapter 30 uses it to check the values first.
Common mistake: forgetting the receiver
Leave the receiver off, and you have a plain function that knows
nothing about c.
exercises/26/06-missing-receiver.go has this mistake on
purpose:
func step() counter {
c.count = c.count + 1
return c
}
func main() {
c := counter{}
c = c.step()
fmt.Println(c.count)
}Running it stops with:
# command-line-arguments
exercises\26\06-missing-receiver.go:17:2: undefined: c
exercises\26\06-missing-receiver.go:18:9: undefined: c
exercises\26\06-missing-receiver.go:23:8: c.step undefined (type counter has no field or method step)
The first two errors say that step has no
c. The last says that counter has no method
step, because step is not attached to it. The
fix is to add the receiver:
func (c counter) step() counter {.
Homework
Homework starters are in exercises/26/homework/. Each
one runs as it is and prints a placeholder line. Define your types and
methods outside main. All the methods in this homework have
value receivers, so the ones that change something give back the changed
copy.
Problem 1 — Counter
Open exercises/26/homework/01-counter-object.go. Build a
counter struct with one field and three methods:
count: an int field, which starts at its zero value.inc(): gives back a counter withcountincreased by 1.get(): returns the current count.reset(): gives back a counter withcountset to0.
Call them in sequence, and print the value at each step.
Problem 2 — Dog with bark
Open exercises/26/homework/02-dog.go. Build a
dog struct with a name field and a
bark() method that prints <name>: Woof!.
Make a second dog with a different name. Call bark() on
both, and confirm that each prints its own name.
Problem 3 — Calculator
Open exercises/26/homework/03-calculator.go. Build a
calc struct with:
value: an int field, which starts at its zero value.add(n): gives back a calc withnadded.sub(n): gives back a calc withnsubtracted.mul(n): gives back a calc multiplied byn.show(): prints the current value with a label, asValue: 10.
Make a few calls, and confirm that the value updates as expected.
Challenge — Stack
Open exercises/26/homework/04-stack.go. Build a
stack struct that holds strings, and behaves like a stack,
where the last item in is the first out:
items: a[]stringfield.push(v): gives back a stack withvon top.pop(): gives back two values, a stack without the top item, and the top item.peek(): returns the top item andtrue, or""andfalseif the stack is empty, like thev, okform from Chapter 23.size(): returns how many items are stored.
The top of the stack is the end of the slice. Use append
to push, and slicing from Chapter 22 to pop.
Stuck or finished? Open the homework solutions page.