Problem overview
A company pays every employee a salary. A manager is an employee too, but earns a bonus on top. The lesson asks for the code that models this, and a function describe that turns any worker into text such as Ana earns 3000.
Go has no classes and no extends. Two other features do the job:
- Embedding. A struct can list another type, with no field name, among its fields. It then gets that type's fields and methods as if they were its own.
- Interfaces. An interface lists methods. Any type that has those methods satisfies the interface automatically, with no
implementsdeclaration.
The pieces are the Employee struct with NewEmployee, getName() and pay(); the Manager struct, which embeds Employee, adds a bonus and has its own pay(); the interface Worker; and describe(w Worker).
For an employee Ana with salary 3000 the description is Ana earns 3000. For a manager Bob with salary 4000 and bonus 1000 it is Bob earns 5000.
Approach
Employee is simple: it stores a name and a salary, and its pay is the salary.
Manager embeds Employee, so it already has a name, a salary and getName() without writing any of them again. It stores only what an employee doesn't know: the bonus. Its pay() has the same name as the employee's, which shadows the embedded one: calling pay() on a manager runs the manager's version. The manager's version calls the employee's through the embedded field, this.Employee.pay(), and adds the bonus. That way the code that knows how an employee is paid stays in one place.
Writing this.pay() inside Manager.pay would be a mistake: it calls the same method again, which calls itself again, until the program runs out of stack.
Finally, describe must work for both types. It takes a Worker, an interface with getName() and pay(). Both structs have those methods, so both fit. Calling w.pay() through the interface runs the method of the value inside: the manager's for a manager, the employee's for an employee.
Why describe is a function, not a method
In some languages, describe would be a method of the parent class that managers inherit, and its call to pay() would pick the manager's version. Go's embedding doesn't work that way. Inside a method of Employee, this is the embedded Employee, which knows nothing about the manager around it. If the method were written as func (this Employee) describe() string and called on Manager{Employee{"Bob", 4000}, 1000}, it would report Bob earns 4000, while the manager's pay() is 5000. A function that takes the interface avoids the problem, because the interface value remembers which type it holds.
Step by step
- Employee fields. Declare
nameas astringandsalaryas anint. - Employee constructor.
NewEmployeereturnsEmployee{name: name, salary: salary}. getName. Returnthis.name. The interface lists methods, not fields, sodescribeneeds this method to reach the name.pay. Returnthis.salary.- Embed in Manager. Write
Employeealone on a line inside the struct. - Add the bonus. Declare
bonusas anint. - Manager constructor. Return a
Managerwhose embedded part is built byNewEmployee(name, salary). An embedded field is named after its type, so it is set withEmployee: .... - Manager's
pay. Returnthis.Employee.pay() + this.bonus. - Describe. Return the text built by
fmt.Sprintfwith the format"%s earns %d",w.getName()andw.pay().
The solution in Go
type Employee struct {
name string
salary int
}
func NewEmployee(name string, salary int) Employee {
return Employee{name: name, salary: salary}
}
func (this Employee) getName() string {
return this.name
}
func (this Employee) pay() int {
return this.salary
}
type Manager struct {
Employee
bonus int
}
func NewManager(name string, salary int, bonus int) Manager {
return Manager{Employee: NewEmployee(name, salary), bonus: bonus}
}
func (this Manager) pay() int {
return this.Employee.pay() + this.bonus
}
type Worker interface {
getName() string
pay() int
}
func describe(w Worker) string {
return fmt.Sprintf("%s earns %d", w.getName(), w.pay())
}
Go details:
- Promotion.
getName()is written only forEmployee, yetManagerhas it too, because methods of an embedded type are promoted. Fields are promoted as well: for a managerm,m.salaryis the embedded employee's salary. - Shadowing is not overriding. The manager's
payhides the employee's for calls made on aManager(or through an interface that holds one). The employee's method is still there and reachable asm.Employee.pay(). - Implicit interfaces. Neither struct mentions
Worker. Each simply has both methods with the right signatures. If a struct lacked one, passing it todescribewould be a compile error. fmt.Sprintf. It builds text likefmt.Printfprints it, but returns the string.%stakes a string and%dan integer, in order.
Using it
fmt.Println(describe(NewEmployee("Ana", 3000)))
fmt.Println(describe(NewManager("Bob", 4000, 1000)))
fmt.Println(describe(NewManager("Cleo", 5000, 0)))
m := NewManager("Eve", 7000, 2500)
fmt.Println(m.getName(), m.pay(), m.Employee.pay(), m.salary)
var w Worker = m
fmt.Println(w.pay())
This prints:
Ana earns 3000
Bob earns 5000
Cleo earns 5000
Eve 9500 7000 7000
9500
A manager with a bonus of 0 earns exactly the salary, like an employee. For Eve, m.pay() is 9500, the salary plus the bonus, while m.Employee.pay() is just the salary 7000. Storing the manager in a Worker variable keeps the manager's behaviour: w.pay() is still 9500.
Edge cases and common mistakes
- Calling
this.pay()in the manager'spay. It calls itself and never stops. - Repeating the salary logic. Returning
this.salary + this.bonusworks today but copies the idea of how an employee is paid. Calling the employee'spaykeeps it in one place. - Making
describea method ofEmployee. It would always use the employee's pay, even for a manager. - Forgetting to build the embedded part. A
Managerbuilt without settingEmployeehas an empty name and a salary of 0. - Zero bonus and equal names. A bonus of 0 is valid, and two people may share a name, since each value keeps its own data.
