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.

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) // 60

This 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)) // 3

The 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 arrows

Chapter 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 with count increased by 1.
  • get(): returns the current count.
  • reset(): gives back a counter with count set to 0.

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 with n added.
  • sub(n): gives back a calc with n subtracted.
  • mul(n): gives back a calc multiplied by n.
  • show(): prints the current value with a label, as Value: 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 []string field.
  • push(v): gives back a stack with v on top.
  • pop(): gives back two values, a stack without the top item, and the top item.
  • peek(): returns the top item and true, or "" and false if the stack is empty, like the v, ok form 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.