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-inmaxfunction 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 withsyntax 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. Theappendis lost when the method ends, and the stack stays empty. - Naming the field
Items. It would then be exported, andmaincould 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.