30. Designing a small type — Homework solutions

The .go solution files are in exercises/30/homework/solutions/. The challenge is a module, so go into its folder to run it:

cd exercises/30/homework/solutions/04-stack
go run .

Problem 1 — Counter

Problem. A counter with increment, get and reset.

How to think about it. It knows a count. It can increment, report and reset. Its rule is that the count only changes through those methods. Two of them change the count, so all three have pointer receivers. The zero value already starts at 0, so counter{} is ready to use, and no newCounter is needed. This file is in package main, so Go does not enforce the rule. Only a separate package would.

Worked solution.

package main

import "fmt"

type counter struct {
    count int
}

func (c *counter) increment() {
    c.count = c.count + 1
}

func (c *counter) get() int {
    return c.count
}

func (c *counter) reset() {
    c.count = 0
}

func main() {
    c := counter{}
    c.increment()
    c.increment()
    c.increment()
    fmt.Println(c.get()) // 3

    c.reset()
    fmt.Println(c.get()) // 0
}

Problem 2 — Clamped health

Problem. Health that stays between 0 and maxHealth.

How to think about it. Enforce the rule inside heal and damage, so that no caller can push health out of range. A negative n needs its own check. Without it, heal(-1000) would pass the upper limit check and drive health below 0. A bad maxHealth is refused in newHealth, so no health value ever starts out wrong. The small check function saves repeating the same if for each call in main.

Worked solution.

package main

import "fmt"

type health struct {
    current   int
    maxHealth int
}

func newHealth(maxHealth int) (*health, error) {
    if maxHealth <= 0 {
        return nil, fmt.Errorf("the maximum %d is not above 0", maxHealth)
    }
    return &health{current: maxHealth, maxHealth: maxHealth}, nil
}

func (h *health) heal(n int) error {
    if n < 0 {
        return fmt.Errorf("cannot heal by %d", n)
    }
    h.current = h.current + n
    if h.current > h.maxHealth {
        h.current = h.maxHealth
    }
    return nil
}

func (h *health) damage(n int) error {
    if n < 0 {
        return fmt.Errorf("cannot damage by %d", n)
    }
    h.current = h.current - n
    if h.current < 0 {
        h.current = 0
    }
    return nil
}

func (h *health) get() int {
    return h.current
}

// check prints an error, if there is one.
func check(err error) {
    if err != nil {
        fmt.Println("Error:", err)
    }
}

func main() {
    _, err := newHealth(0)
    fmt.Println(err) // the maximum 0 is not above 0

    h, err := newHealth(100)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }

    check(h.damage(30))
    fmt.Println(h.get()) // 70

    check(h.heal(1000))
    fmt.Println(h.get()) // 100

    check(h.damage(9999))
    fmt.Println(h.get()) // 0

    check(h.heal(-5))    // Error: cannot heal by -5
    fmt.Println(h.get()) // 0
}

Common mistakes.

  • Naming a parameter max. It compiles, but it hides Go's built-in max function inside that function. A longer name is clearer.

Problem 3 — Light switch

Problem. A switch with toggle and isOn.

How to think about it. It knows one bool. The zero value of a bool is false, so lightSwitch{} starts off with no further work.

Worked solution.

package main

import "fmt"

type lightSwitch struct {
    on bool
}

func (s *lightSwitch) toggle() {
    s.on = !s.on
}

func (s *lightSwitch) isOn() bool {
    return s.on
}

func main() {
    s := lightSwitch{}
    fmt.Println(s.isOn()) // false

    s.toggle()
    fmt.Println(s.isOn()) // true

    s.toggle()
    fmt.Println(s.isOn()) // false
}

s.on = !s.on flips the bool on each call, which is the whole switch in one line.

Common mistakes.

  • Writing type switch struct. Go stops with syntax error: unexpected switch, expected name.
  • Using a value receiver on toggle. It flips a copy, and the switch stays off.

Challenge — Stack with a guard

Problem. Pop() on an empty stack returns an error instead of crashing.

How to think about it. Without a guard, reading the top of an empty slice crashes, as you saw in Chapter 26. The guard checks the length first, and that is the rule the method protects. items is lower case, so main cannot touch the slice. Pop has an item or a problem to report, so it returns (string, error), and "" fills the place of the item on failure.

Worked solution. stack/stack.go:

package stack

import "errors"

// Stack is a stack of strings. Make one with New.
type Stack struct {
    items []string
}

// New returns an empty stack.
func New() *Stack {
    return &Stack{}
}

// Push adds v on top.
func (s *Stack) Push(v string) {
    s.items = append(s.items, v)
}

// Pop removes and returns the top item, or returns an error if the stack is
// empty.
func (s *Stack) Pop() (string, error) {
    if len(s.items) == 0 {
        return "", errors.New("the stack is empty")
    }
    last := len(s.items) - 1
    top := s.items[last]
    s.items = s.items[:last]
    return top, nil
}

// Size returns the number of items.
func (s *Stack) Size() int {
    return len(s.items)
}

main.go:

package main

import (
    "fmt"

    "stackmodule/stack"
)

func main() {
    s := stack.New()
    s.Push("a")
    s.Push("b")
    fmt.Println(s.Size())

    for i := 0; i < 3; i++ {
        top, err := s.Pop()
        if err != nil {
            fmt.Println("Error:", err)
            continue
        }
        fmt.Println(top)
    }

    fmt.Println(s.Size())
}

Output:

2
b
a
Error: the stack is empty
0

Common mistakes.

  • Using a value receiver on Push. The append is lost when the method ends, and the stack stays empty.
  • Naming the field Items. It would then be exported, and main could change the slice directly, past the guard.

Done?

That is the end of the Part 6 chapters. You can build structs, make many of them, share code with embedding, treat types alike with interfaces, and design a safe public surface. Part 6 has two mini-projects, the Inventory System and the Monster Battle, both built from cooperating values.