Problem overview
A shop keeps track of how many pieces of each item it has: the item's stock. The stock can never go below 0, and it changes only when pieces are sold or new ones arrive. So the stock is hidden and changed only through two methods that check the rules. The item's name has no rules to protect, so any code may read it.
In Go, a name that starts with an uppercase letter is exported: code outside the package can use it. A name that starts with a lowercase letter is unexported: only code inside the package can. The lesson uses both on one struct: Name is exported, stock is not.
The methods are:
NewInventoryItem(name, stock)returns an item with that name and stock.sell(n)sellsnpieces and returnstrueifnis at least 1 and at most the stock. Otherwise it changes nothing and returnsfalse.restock(n)addsnpieces whennis at least 1 and ignores it otherwise.getStock()returns the stock.
Approach
Each method checks its rule before it changes anything.
For sell, it is easier to describe the refusals than the successes. A sale is refused when n is below 1 (selling 0 or a negative number makes no sense) or above the stock (you can't sell what you don't have). The test uses ||, "or", and returns false right away, before the stock is touched, so a refused sale leaves everything as it was. Selling exactly the whole stock is allowed. Past the check, the sale goes through: lower the stock by n and return true. Because the check passed, the stock can't drop below 0.
For restock, one if is enough: add n only when it is at least 1. A delivery of 0 or a negative number is not a delivery.
Every method is constant time.
Step by step
- Declare
Nameas astring, with an uppercase first letter, so other code can read it. - Declare
stockas anint, lowercase, so onlysellandrestockchange it from outside. - Write the constructor. It returns an item with
Name: nameandstock: stock. In each pair, the first is the field and the second is the parameter. - Refuse bad sales. In
sell, aniffornbelow 1 or abovethis.stockreturnsfalse. - Complete the sale. After the
if, subtractnfromthis.stockand returntrue. - Write
restock. Ifn >= 1, add it to the stock. - Write
getStock, which returnsthis.stock.
The solution in Go
type InventoryItem struct {
Name string
stock int
}
func NewInventoryItem(name string, stock int) InventoryItem {
return InventoryItem{Name: name, stock: stock}
}
func (this *InventoryItem) sell(n int) bool {
if n < 1 || n > this.stock {
return false
}
this.stock -= n
return true
}
func (this *InventoryItem) restock(n int) {
if n >= 1 {
this.stock += n
}
}
func (this InventoryItem) getStock() int {
return this.stock
}
Go details:
- The first letter decides. There is no
publicorprivatekeyword.Nameandstockdiffer only in the case of their first letter. - Pointer receivers.
sellandrestockchange the item, so they use(this *InventoryItem).getStockonly reads, so a value receiver is enough. - Early return. The refusal comes first and returns, so the rest of the method needs no
else. Go code often puts the failure cases at the top. - Short-circuit
||. Ifn < 1is true, Go doesn't evaluate the second part. Both parts are simple here, so it only affects speed, never the answer.
Why the check lives in the methods
If the stock were exported, any code could write item.Stock = -50 and nothing would notice. Hiding the field forces every change through sell and restock, which are the only places that need to know the rules. The same reasoning explains why Name stays exported: reading the name can't break anything, and a getter method would add code without adding safety.
Note that the restriction is by package. A test written in the same package can still reach stock. The hiding protects code that imports the package, which is how Go programs are normally split.
Using it
p := NewInventoryItem("Pencil", 5)
fmt.Println(p.Name, p.sell(3), p.getStock(), p.sell(5), p.getStock())
p.restock(10)
fmt.Println(p.getStock())
l := NewInventoryItem("Lamp", 2)
fmt.Println(l.sell(0), l.sell(-3))
l.restock(0)
l.restock(-5)
fmt.Println(l.getStock())
l.restock(1)
fmt.Println(l.sell(3), l.getStock())
fmt.Println(l.sell(1), l.getStock())
This prints:
Pencil true 2 false 2
12
false false
2
true 0
false 0
Selling 3 of the 5 pencils works and leaves 2. Selling 5 more is refused because only 2 are left, and the stock stays 2. After restock(10) it is 12. The lamps show the refusals: selling 0 or -3 fails, restocking 0 or -5 is ignored, and the stock stays 2. After restock(1) there are 3, and selling all 3 works. With nothing left, even sell(1) is refused. The lesson's tests print the booleans as True and False; Go itself prints true and false.
Edge cases and common mistakes
- Selling the whole stock.
nequal tothis.stockis allowed. Writingn >= this.stockin the refusal would wrongly refuse it. - Changing the stock before checking. Subtracting first and testing afterwards leaves a refused sale with a changed stock.
- Allowing 0 or negative sales. A negative
sellwould raise the stock, and a negativerestockwould lower it. Both checks exist to prevent that. - A value receiver on
sellorrestock. It compiles, but the stock never changes. - An uppercase
Stock. It would be exported, and outside code could set it to -50 directly.
