Problem overview
A game shows each player two numbers: the total of all their turns, and their best single turn. The ScoreKeeper in this lesson keeps both up to date as points come in, and can start over.
It has five parts: NewScoreKeeper() makes a keeper with no points, add(points) records one turn, total() returns the sum, best() returns the highest single turn (0 if there was none), and reset() starts over.
After add(10), add(25) and add(5), total() is 40 and best() is 25.
Approach
You could store every turn in a list and work the answers out on request. That works, but it keeps growing and each call has to look through the list. The better idea is to keep only the two numbers the questions are about and update them as each turn arrives:
- the total grows by the new points every time;
- the best changes only when the new points are higher than the best so far.
The best cannot be worked out from the total later on, which is why it needs its own field. With both fields kept current, total and best just report them. Every method runs in constant time.
Starting the best at 0 is safe: points are never negative, so any first turn is at least 0, and a keeper with no turns must report a best of 0 anyway.
Step by step
- Declare
totalPoints, anintfor the running sum. The field can't be calledtotal, because a struct can't have a field and a method with the same name. - Declare
bestPoints, the highest single add so far. - Write the constructor, setting both fields to 0. A new keeper has seen no points.
- In
add, add the points tototalPoints. The method changes the struct, so its receiver is a pointer. - Update the best. If
pointsis greater thanbestPoints, store it. With>, an equal score leaves the best as it is, which is the same number anyway. - Write
totalandbest. Each returns its field. They only read, so a value receiver is enough. - Write
reset. Set both fields back to 0, the values a new keeper has, so the best only counts adds that come later.
The solution in Go
type ScoreKeeper struct {
totalPoints int
bestPoints int
}
func NewScoreKeeper() ScoreKeeper {
return ScoreKeeper{totalPoints: 0, bestPoints: 0}
}
func (this *ScoreKeeper) add(points int) {
this.totalPoints += points
if points > this.bestPoints {
this.bestPoints = points
}
}
func (this ScoreKeeper) total() int {
return this.totalPoints
}
func (this ScoreKeeper) best() int {
return this.bestPoints
}
func (this *ScoreKeeper) reset() {
this.totalPoints = 0
this.bestPoints = 0
}
Pointer receivers and value receivers
This is the Go detail the lesson is about. A method with a value receiver, (this ScoreKeeper), works on a copy of the struct. Changes made to the copy vanish when the method returns. A method with a pointer receiver, (this *ScoreKeeper), works on the original, so its changes stay.
That is why add and reset use pointers and total and best don't. If add had a value receiver, it would compile and run, but nothing would be recorded: after add(10) the field would still be 0.
You don't have to write (&k).add(10). When k is a variable, k.add(10) takes its address for you.
Why not store every turn
Keeping a list of all turns would also give the right answers, but add would grow the list for ever, total would have to add the whole list up on each call, and best would have to scan it. Two integers carry all the information the two questions need. When a struct only has to answer a few questions, ask which facts those questions depend on and store just those.
Using it
k := NewScoreKeeper()
fmt.Println(k.best(), k.total())
k.add(30)
k.add(50)
fmt.Println(k.total(), k.best())
k.reset()
fmt.Println(k.total(), k.best())
k.add(20)
fmt.Println(k.best(), k.total())
This prints:
0 0
80 50
0 0
20 20
After the reset, the earlier 50 no longer counts, so the best after add(20) is 20, not 50. Two keepers are independent: after k2 := NewScoreKeeper() and k2.add(10), k.total() is still 20 while k2.total() is 10.
What a value receiver does to add
Suppose a struct BadKeeper with a field totalPoints has this method:
func (this BadKeeper) add(p int) { this.totalPoints += p }
After b := BadKeeper{} and b.add(10), printing b.totalPoints gives 0. The method added 10 to its own copy, and the copy was thrown away when the method returned. Go reports no error, so the only sign is a total that never moves.
The fix is the pointer receiver. With (this *BadKeeper), the method receives the address of b and the 10 stays.
Edge cases and common mistakes
- A value receiver on
addorreset. The code compiles, but the changes are lost on a copy. - Not resetting the best. A
resetthat clears only the total leaves the old best behind. - Calling
best()before any add. It returns 0, because the field starts at 0. - A turn worth 0.
add(0)changes neither number. - Naming a field like a method. A field
totalnext to a methodtotal()is a compile error, hencetotalPointsandbestPoints.
