DEV Community

Saloni Agarwal
Saloni Agarwal

Posted on

Composition Over Inheritance in Go

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")
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • Engineer and Manager embed Payable, giving them a Salary.
  • Manager also embeds Manageable, giving them team management abilities.
  • Each type implements Work() individually, satisfying the Worker 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)
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)