When designing software, the "composition over inheritance" principle often leads to more flexible, maintainable code. Go, with its unique approach to object-oriented design, leans heavily into composition rather than inheritance. Let's see why.
Why Go Prefers Composition
In traditional OOP languages, inheritance lets one class inherit behaviors and properties from another, but this can lead to rigid, hard-to-change hierarchies. Go avoids inheritance altogether and instead encourages composition—where types are built by combining smaller, focused components.
Composition in Action
Imagine we’re modeling a company with different types of workers: some are engineers, some are managers, and some are interns. Instead of creating a complex class hierarchy, we’ll define specific behaviors as independent types and then compose them.
Example: Worker Behaviors in Go
package main
import "fmt"
type Worker interface {
Work()
}
type Payable struct {
Salary int
}
func (p Payable) GetSalary() int {
return p.Salary
}
type Manageable struct{}
func (m Manageable) Manage() {
fmt.Println("Managing team")
}
type Engineer struct {
Payable
}
func (e Engineer) Work() {
fmt.Println("Engineering work being done")
}
type Manager struct {
Payable
Manageable
}
func (m Manager) Work() {
fmt.Println("Managerial work being done")
}
Here:
-
Engineer
andManager
embedPayable
, giving them aSalary
. -
Manager
also embedsManageable
, giving them team management abilities. - Each type implements
Work()
individually, satisfying theWorker
interface.
Benefits of Composition
- Simplicity: Each behavior is encapsulated in its struct, making it easy to extend.
- Flexibility: New types or behaviors can be added without breaking existing code.
- Reusability: Behaviors are modular, so we can easily combine them in different ways.
Using Interfaces with Composition
In Go, interfaces and composition work together to allow polymorphism without inheritance. Here’s how we can handle multiple worker types with a single function:
func DescribeWorker(w Worker) {
w.Work()
}
func main() {
engineer := Engineer{Payable{Salary: 80000}}
manager := Manager{Payable{Salary: 100000}, Manageable{}}
DescribeWorker(engineer)
DescribeWorker(manager)
}
Go’s preference for composition over inheritance isn’t just a language quirk—it encourages cleaner, more modular code that’s adaptable to change. Instead of rigid hierarchies, you get flexible, reusable components that keep your codebase nimble and easy to maintain.