27. Pointers — Homework solutions

The .go solution files are in exercises/27/homework/solutions/.

Problem 1 — Point with move

Problem. A point with distance and move.

How to think about it. Start from the chapter's point. move changes the fields, so it needs a pointer receiver, and distance gets one too, to match. a.move(1, 1) works because a is a variable, and Go takes its location for you.

Worked solution.

package main

import (
    "fmt"
    "math"
)

type point struct {
    x float64
    y float64
}

func (p *point) distance(other point) float64 {
    dx := p.x - other.x
    dy := p.y - other.y
    return math.Sqrt(dx*dx + dy*dy)
}

func (p *point) move(dx float64, dy float64) {
    p.x = p.x + dx
    p.y = p.y + dy
}

func main() {
    a := point{x: 0, y: 0}
    b := point{x: 3, y: 4}
    fmt.Println(a.distance(b)) // 5

    a.move(1, 1)
    fmt.Println(a.x, a.y)               // 1 1
    fmt.Printf("%.3f\n", a.distance(b)) // 3.606
}

Common mistakes.

  • Writing func (p point) move(...) with no *. It compiles, but the move changes a copy, and a stays where it was.
  • Forgetting to add "math" to the imports. Go stops with undefined: math.

Problem 2 — Character

Problem. A character with damage, healing and a report line.

How to think about it. Two rules go with hp: never below 0, and never above maxHP. Each one has a single place to live, an if at the end of the method that changes hp. report calls isAlive on the same receiver.

Worked solution.

package main

import "fmt"

type character struct {
    name  string
    hp    int
    maxHP int
}

func newCharacter(name string, hp int) *character {
    return &character{name: name, hp: hp, maxHP: hp}
}

func (c *character) takeDamage(amount int) {
    c.hp = c.hp - amount
    if c.hp < 0 {
        c.hp = 0
    }
}

func (c *character) heal(amount int) {
    c.hp = c.hp + amount
    if c.hp > c.maxHP {
        c.hp = c.maxHP
    }
}

func (c *character) isAlive() bool {
    return c.hp > 0
}

func (c *character) report() {
    fmt.Printf("%s: %d / %d HP (alive: %v)\n", c.name, c.hp, c.maxHP, c.isAlive())
}

func main() {
    keiko := newCharacter("Keiko", 100)
    keiko.report()

    keiko.takeDamage(30)
    keiko.report()

    keiko.takeDamage(80)
    keiko.report()

    keiko.heal(20)
    keiko.report()
}

Output:

Keiko: 100 / 100 HP (alive: true)
Keiko: 70 / 100 HP (alive: true)
Keiko: 0 / 100 HP (alive: false)
Keiko: 20 / 100 HP (alive: true)

Common mistakes.

  • Letting hp go below 0 or above maxHP. The two if checks in takeDamage and heal hold the value in range.
  • Setting hp in newCharacter and forgetting maxHP. Its zero value is 0, so the first heal drops hp to 0.

Problem 3 — Rectangle

Problem. A rectangle with area and perimeter.

How to think about it. No method changes the rectangle, and it has only two small fields. Rule 3 from the chapter applies, so value receivers fit. Pointer receivers would also work, and the output would be the same.

Worked solution.

package main

import "fmt"

type rectangle struct {
    w int
    h int
}

func (r rectangle) area() int {
    return r.w * r.h
}

func (r rectangle) perimeter() int {
    return 2 * (r.w + r.h)
}

func main() {
    r1 := rectangle{w: 3, h: 4}
    r2 := rectangle{w: 10, h: 2}

    fmt.Println(r1.area())      // 12
    fmt.Println(r1.perimeter()) // 14
    fmt.Println(r2.area())      // 20
    fmt.Println(r2.perimeter()) // 24
}

Common mistakes.

  • Keeping w and h in variables outside the functions. With two rectangles, the second would overwrite the first. The fields belong to each value.

Challenge — Dog and friend

Problem. A dog whose friend is a pointer to another dog, or nil.

How to think about it. A *dog field means "leads to a dog, or to nothing". newDog leaves it out, so it starts as nil. describe must check for nil before reading d.friend.name.

Worked solution.

package main

import "fmt"

type dog struct {
    name   string
    friend *dog
}

func newDog(name string) *dog {
    return &dog{name: name}
}

func (d *dog) describe() {
    if d.friend == nil {
        fmt.Printf("%s has no friend.\n", d.name)
        return
    }
    fmt.Printf("%s is friends with %s.\n", d.name, d.friend.name)
}

func main() {
    rex := newDog("Rex")
    lassie := newDog("Lassie")
    rex.describe()
    lassie.describe()

    rex.friend = lassie
    rex.describe()
    lassie.describe()

    rex.friend.name = "Lady"
    rex.describe()
    lassie.describe()
}

Output:

Rex has no friend.
Lassie has no friend.
Rex is friends with Lassie.
Lassie has no friend.
Rex is friends with Lady.
Lady has no friend.

The last two lines show the sharing. rex.friend and lassie lead to the same dog, so changing the name through one shows through the other. A dog field in place of the *dog would not even compile, with invalid recursive type: dog refers to itself, because a dog would have to contain a whole dog, which contains a whole dog, without end. A pointer has a fixed size.

Common mistakes.

  • Reading d.friend.name with no nil check. The first call to describe crashes with invalid memory address or nil pointer dereference.

Done?

You can now change a value through a pointer, and choose a receiver for a method. Next, Many structs together puts structs in slices and maps, where the copy from Chapter 26 comes back as a trap.