DEV Community

Cover image for From PHP to Go: Recognizing and Avoiding PHP-ish Patterns in Go Projects (part 1)
Mohammad Reza
Mohammad Reza

Posted on

From PHP to Go: Recognizing and Avoiding PHP-ish Patterns in Go Projects (part 1)

Introduction

As developers, we carry mental models and habits from one language to another. While this cross-pollination of ideas can sometimes enhance our work, it often leads to subtle misalignments when adopting a new language—especially when transitioning from a highly dynamic and flexible language like PHP to a statically typed, minimalist language like Go.

You may have heard comments like, "This Go project feels PHP-ish" or "A PHP developer must have written this." These remarks often highlight deeper issues: a mismatch between the principles of Go and the practices brought over from PHP. This isn’t just about code—it’s about the architectural mindset and how projects are structured and organized.

Why Go Prefers Composition Over Inheritance

When designing extensible systems, one of the most common dilemmas is deciding between inheritance and composition. Inheritance, while familiar to many developers, can lead to rigid hierarchies and exponential complexity as new behaviors are added. Go’s philosophy favors composition, enabling flexibility, modularity, and scalability.

In this article, we’ll explore this comparison through the example of a Task Execution System. We’ll build the system using both inheritance and composition, compare the results, and highlight why Go's composition-first philosophy is often the better choice.

Scenario: A Task Execution System

We want to design a system where:

  • Basic tasks like EmailTask and DatabaseTask can be executed.
  • Additional behaviors like notifications and retries can be added to tasks.
  • The system should be extensible for future behaviors (e.g., logging, metrics).

Inheritance-Based Approach

Using inheritance, we extend tasks by creating new subclasses to add behaviors. While this approach is familiar, it quickly becomes cumbersome as new behaviors and combinations are introduced.

package main

import "fmt"

// Base Task
type Task struct {
    Name string
}

func (t *Task) Execute() {
    fmt.Println("Executing task:", t.Name)
}

// EmailTask inherits from Task
type EmailTask struct {
    Task
}

func (e *EmailTask) Execute() {
    fmt.Println("Sending email task:", e.Name)
}

// DatabaseTask inherits from Task
type DatabaseTask struct {
    Task
}

func (d *DatabaseTask) Execute() {
    fmt.Println("Executing database task:", d.Name)
}

// Add Notification by Extending Task
type NotifyingEmailTask struct {
    EmailTask
}

func (n *NotifyingEmailTask) Execute() {
    fmt.Println("Sending notification before executing email task...")
    n.EmailTask.Execute()
    fmt.Println("Sending notification after executing email task...")
}

type NotifyingDatabaseTask struct {
    DatabaseTask
}

func (n *NotifyingDatabaseTask) Execute() {
    fmt.Println("Sending notification before executing database task...")
    n.DatabaseTask.Execute()
    fmt.Println("Sending notification after executing database task...")
}

// Add Retry Behavior by Extending Task
type RetryingNotifyingEmailTask struct {
    NotifyingEmailTask
    Retry int
}

func (r *RetryingNotifyingEmailTask) Execute() {
    for i := 0; i < r.Retry; i++ {
        fmt.Printf("Retry %d: ", i+1)
        r.NotifyingEmailTask.Execute()
    }
}

type RetryingNotifyingDatabaseTask struct {
    NotifyingDatabaseTask
    Retry int
}

func (r *RetryingNotifyingDatabaseTask) Execute() {
    for i := 0; i < r.Retry; i++ {
        fmt.Printf("Retry %d: ", i+1)
        r.NotifyingDatabaseTask.Execute()
    }
}

func main() {
    fmt.Println("Executing notifying tasks:")
    notifyingEmailTask := NotifyingEmailTask{EmailTask{Task{Name: "Weekly Newsletter"}}}
    notifyingDbTask := NotifyingDatabaseTask{DatabaseTask{Task{Name: "DB Backup"}}}

    notifyingEmailTask.Execute()
    notifyingDbTask.Execute()

    fmt.Println("\nExecuting retrying notifying tasks:")
    retryingNotifyingEmailTask := RetryingNotifyingEmailTask{
        NotifyingEmailTask: NotifyingEmailTask{EmailTask{Task{Name: "Weekly Newsletter"}}},
        Retry:              3,
    }

    retryingNotifyingDbTask := RetryingNotifyingDatabaseTask{
        NotifyingDatabaseTask: NotifyingDatabaseTask{DatabaseTask{Task{Name: "DB Backup"}}},
        Retry:                 2,
    }

    retryingNotifyingEmailTask.Execute()
    retryingNotifyingDbTask.Execute()
}
Enter fullscreen mode Exit fullscreen mode

Problems with the Inheritance Approach

  • Behavior Explosion: Adding behaviors like retries or notifications requires creating a new subclass for each combination. Example: RetryingNotifyingEmailTask and RetryingNotifyingDatabaseTask.
  • Tight Coupling: Behaviors are tightly bound to specific task types (EmailTask, DatabaseTask).
  • Limited Extensibility:Adding new behaviors (e.g., logging) requires modifying existing classes or creating additional subclasses.

Composition-Based Approach

Using composition, we build the same system by combining smaller, reusable components (decorators). This approach avoids the pitfalls of inheritance.

package main

import "fmt"

// Task interface defines the behavior
type Task interface {
    Execute()
}

// Basic tasks implement the Task interface
type EmailTask struct {
    Name string
}

func (e EmailTask) Execute() {
    fmt.Println("Sending email task:", e.Name)
}

type DatabaseTask struct {
    Name string
}

func (d DatabaseTask) Execute() {
    fmt.Println("Executing database task:", d.Name)
}

// Notifier decorator adds notification behavior
type Notifier struct {
    Task Task
}

func (n Notifier) Execute() {
    fmt.Println("Sending notification before task execution...")
    n.Task.Execute()
    fmt.Println("Sending notification after task execution...")
}

// Retry decorator adds retry behavior
type Retry struct {
    Task  Task
    Retry int
}

func (r Retry) Execute() {
    for i := 0; i < r.Retry; i++ {
        fmt.Printf("Retry %d: ", i+1)
        r.Task.Execute()
    }
}

func main() {
    fmt.Println("Executing notifying tasks:")
    Notifier{Task: EmailTask{Name: "Weekly Newsletter"}}.Execute()
    Notifier{Task: DatabaseTask{Name: "DB Backup"}}.Execute()

    fmt.Println("\nExecuting retrying notifying tasks:")
    Retry{
        Task: Notifier{
            Task: EmailTask{Name: "Weekly Newsletter"},
        },
        Retry: 3,
    }.Execute()

    Retry{
        Task: Notifier{
            Task: DatabaseTask{Name: "DB Backup"},
        },
        Retry: 2,
    }.Execute()
}

Enter fullscreen mode Exit fullscreen mode

The inheritance approach becomes rigid and unmanageable as the number of behaviors grows, while the composition approach:

  • Keeps code modular and reusable.
  • Supports dynamic behavior combination.
  • Simplifies adding new features.

Top comments (0)