Problem overview
A thermostat keeps a target temperature, and the target must always stay between 10 and 30 degrees. If any code could write to the target directly, nothing would stop it from setting 95. The solution is to hide the field and let only the thermostat's own methods change it, each of them keeping the rule.
The lesson asks for a Thermostat struct with one field, target, and these functions and methods:
NewThermostat()returns a thermostat whose target is 20.setTarget(t)sets the target tot, but a value below 10 gives 10 and a value above 30 gives 30.warmer()raises the target by 1 andcooler()lowers it by 1, never leaving the range 10 to 30.getTarget()returns the target. It is the only way for outside code to read it.
Approach
Go has no private keyword. Visibility depends on the first letter of a name: a lowercase first letter makes the field unexported, visible only inside its package. That is how target is hidden. Code in another package has to call getTarget to read it and setTarget, warmer or cooler to change it.
The range rule is written once, in setTarget. A value that is too low is not refused: it becomes the lowest allowed target, 10. A value that is too high becomes 30. Everything else, including 10 and 30 themselves, is kept as it is.
warmer and cooler do not repeat the rule. They call setTarget with the target plus or minus 1, and setTarget turns 31 back into 30 and 9 back into 10. If the range ever changes, there is one place to edit.
All the methods run in constant time.
Step by step
- Declare
targetas anintfield with a lowercase name. - Write the constructor.
NewThermostatreturns a thermostat withtargetset to 20. - Handle values that are too low. In
setTarget, aniffort < 10setsthis.targetto 10. - Handle values that are too high. An
else iffort > 30sets the target to 30. - Keep every other value. The final
elsesets the target tot. The limits 10 and 30 land here and are kept. - Write
warmer, which callsthis.setTarget(this.target + 1). - Write
cooler, which callsthis.setTarget(this.target - 1). - Write
getTarget, which returnsthis.target.
The solution in Go
type Thermostat struct {
target int
}
func NewThermostat() Thermostat {
return Thermostat{target: 20}
}
func (this *Thermostat) setTarget(t int) {
if t < 10 {
this.target = 10
} else if t > 30 {
this.target = 30
} else {
this.target = t
}
}
func (this *Thermostat) warmer() {
this.setTarget(this.target + 1)
}
func (this *Thermostat) cooler() {
this.setTarget(this.target - 1)
}
func (this Thermostat) getTarget() int {
return this.target
}
Go details:
- Pointer receivers for changes.
setTarget,warmerandcooleruse(this *Thermostat), so they change the thermostat itself. With a value receiver they would change a copy and the target would never move. - A value receiver for reading.
getTargetonly reads, so(this Thermostat)is enough. - Calling one method from another. Inside
warmer,thisis a pointer, sothis.setTarget(...)calls the pointer method on the same thermostat. - The package is the boundary. Unexported does not mean private to the type, as in other languages. Any code in the same package can still touch
target. The protection works against code outside the package.
Clamping, not refusing
There are two common ways to handle a bad value. One is to refuse it and leave the target unchanged; the other is to clamp it to the nearest allowed value. This lesson clamps. Setting 35 does not leave the old target in place, it gives 30. That is also why warmer at 30 can simply ask for 31 and let the clamp bring it back: the result is 30, which is exactly what "never goes above 30" means.
The three branches cover every integer exactly once: below 10, above 30, and everything from 10 to 30 inclusive. Because the branches are chained with else, only one of them runs per call.
Using it
t := NewThermostat()
fmt.Println(t.getTarget())
t.warmer()
t.warmer()
fmt.Println(t.getTarget())
t.cooler()
fmt.Println(t.getTarget())
t.setTarget(35)
fmt.Println(t.getTarget())
t.warmer()
fmt.Println(t.getTarget())
t.setTarget(4)
t.cooler()
fmt.Println(t.getTarget())
t.setTarget(10)
t.warmer()
fmt.Println(t.getTarget())
This prints:
20
22
21
30
30
10
11
A new thermostat starts at 20, two warmer() calls make 22 and cooler() brings it to 21. Setting 35 gives 30, and warmer() at 30 leaves it at 30. Setting 4 gives 10, cooler() at 10 leaves it there, and after setTarget(10) a warmer() reaches 11. Even setTarget(-1000) ends at 10.
Edge cases and common mistakes
- Using
<=or>=. The limits 10 and 30 are allowed. Writingt <= 10would still give 10 here, but the intent is clearer with strict comparisons. - Refusing instead of clamping. Out-of-range values are not ignored. 35 becomes 30, not a no-op.
- Repeating the range in
warmerandcooler. Copies of the rule drift apart. Going throughsetTargetkeeps it in one place. - A value receiver on a changing method. The code compiles, but nothing changes.
- An uppercase field name.
Targetwould be exported, and outside code could set it to 95 and skip every check.
