Beyond Rigid If-Else Logic

Traditional approaches to varying behavior use conditional statements scattered throughout code. Need different sorting algorithms? Different pricing strategies? Different attack behaviors in a game? You end up with messy switch statements and tightly coupled code.

The Strategy pattern lets you encapsulate algorithms and swap them at runtime. In Go, first-class functions make this pattern beautifully simple—no heavyweight class hierarchies required.

The Traditional Approach: Conditionals Everywhere

Here’s what happens without the Strategy pattern:

type PaymentProcessor struct {
    paymentMethod string
}

func (p *PaymentProcessor) ProcessPayment(amount float64) error {
    switch p.paymentMethod {
    case "credit_card":
        // Credit card logic
        fmt.Printf("Processing $%.2f via credit card\n", amount)
        // Validate card, charge, etc.

    case "paypal":
        // PayPal logic
        fmt.Printf("Processing $%.2f via PayPal\n", amount)
        // OAuth flow, API call, etc.

    case "cryptocurrency":
        // Crypto logic
        fmt.Printf("Processing $%.2f via cryptocurrency\n", amount)
        // Blockchain transaction, etc.

    default:
        return fmt.Errorf("unknown payment method: %s", p.paymentMethod)
    }

    return nil
}

// Problem: Adding new payment methods requires modifying this function
// Problem: Testing individual payment methods is difficult
// Problem: Can't swap strategies at runtime easily

Strategy Pattern with First-Class Functions

Let’s refactor using functions as strategies:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

// PaymentStrategy is a function type that processes payments
type PaymentStrategy func(amount float64) error

// Concrete strategies as functions
func CreditCardStrategy(amount float64) error {
    fmt.Printf("Processing $%.2f via Credit Card\n", amount)
    fmt.Println("  - Validating card number")
    fmt.Println("  - Checking CVV")
    fmt.Println("  - Charging card")
    fmt.Println("  - Payment successful!")
    return nil
}

func PayPalStrategy(amount float64) error {
    fmt.Printf("Processing $%.2f via PayPal\n", amount)
    fmt.Println("  - Redirecting to PayPal")
    fmt.Println("  - User authorizes payment")
    fmt.Println("  - Payment captured")
    return nil
}

func CryptocurrencyStrategy(amount float64) error {
    fmt.Printf("Processing $%.2f via Cryptocurrency\n", amount)
    fmt.Println("  - Generating wallet address")
    fmt.Println("  - Creating blockchain transaction")
    fmt.Println("  - Waiting for confirmation")
    fmt.Println("  - Transaction confirmed!")
    return nil
}

func BankTransferStrategy(amount float64) error {
    fmt.Printf("Processing $%.2f via Bank Transfer\n", amount)
    fmt.Println("  - Validating bank account")
    fmt.Println("  - Initiating ACH transfer")
    fmt.Println("  - Transfer scheduled")
    return nil
}

// PaymentProcessor now uses a strategy
type PaymentProcessor struct {
    strategy PaymentStrategy
}

func NewPaymentProcessor(strategy PaymentStrategy) *PaymentProcessor {
    return &PaymentProcessor{strategy: strategy}
}

func (p *PaymentProcessor) SetStrategy(strategy PaymentStrategy) {
    p.strategy = strategy
    fmt.Println("Payment strategy updated")
}

func (p *PaymentProcessor) Process(amount float64) error {
    if p.strategy == nil {
        return fmt.Errorf("no payment strategy set")
    }
    return p.strategy(amount)
}

func main() {
    fmt.Println("=== Strategy Pattern Demo ===\n")

    // Create processor with credit card strategy
    processor := NewPaymentProcessor(CreditCardStrategy)

    fmt.Println("--- Payment 1 ---")
    processor.Process(99.99)

    fmt.Println("\n--- Switching to PayPal ---")
    processor.SetStrategy(PayPalStrategy)
    processor.Process(49.99)

    fmt.Println("\n--- Switching to Cryptocurrency ---")
    processor.SetStrategy(CryptocurrencyStrategy)
    processor.Process(199.99)

    fmt.Println("\n=== Demo Complete ===")
}

Real-World Example: Combat System

Let’s build a flexible combat system with swappable attack strategies:

package main

import (
    "fmt"
    "math"
    "math/rand"
)

// AttackStrategy defines how damage is calculated
type AttackStrategy func(baseDamage float64, target *Character) float64

// Different attack strategies
func NormalAttackStrategy(baseDamage float64, target *Character) float64 {
    damage := baseDamage - target.Defense*0.5
    return math.Max(1, damage)
}

func CriticalAttackStrategy(baseDamage float64, target *Character) float64 {
    // Critical hits ignore 50% of defense and deal 2x damage
    damage := (baseDamage * 2.0) - (target.Defense * 0.25)
    return math.Max(1, damage)
}

func ArmorPiercingStrategy(baseDamage float64, target *Character) float64 {
    // Ignores all armor
    return baseDamage
}

func ElementalAttackStrategy(baseDamage float64, target *Character) float64 {
    // Extra damage based on elemental weakness
    multiplier := 1.0
    if target.ElementWeakness {
        multiplier = 1.5
    }
    damage := (baseDamage * multiplier) - (target.Defense * 0.3)
    return math.Max(1, damage)
}

func LifeStealStrategy(baseDamage float64, target *Character) float64 {
    // Returns damage dealt (caller can use this to heal)
    damage := baseDamage - target.Defense*0.4
    return math.Max(1, damage)
}

// Character with attack strategy
type Character struct {
    Name             string
    Health           float64
    MaxHealth        float64
    Attack           float64
    Defense          float64
    ElementWeakness  bool
    AttackStrategy   AttackStrategy
}

func NewCharacter(name string, health, attack, defense float64) *Character {
    return &Character{
        Name:           name,
        Health:         health,
        MaxHealth:      health,
        Attack:         attack,
        Defense:        defense,
        AttackStrategy: NormalAttackStrategy, // Default strategy
    }
}

func (c *Character) SetAttackStrategy(strategy AttackStrategy) {
    c.AttackStrategy = strategy
}

func (c *Character) AttackTarget(target *Character) {
    if c.AttackStrategy == nil {
        fmt.Printf("%s has no attack strategy!\n", c.Name)
        return
    }

    damage := c.AttackStrategy(c.Attack, target)
    target.TakeDamage(damage)

    fmt.Printf("%s attacks %s for %.1f damage (HP: %.1f/%.1f)\n",
        c.Name, target.Name, damage, target.Health, target.MaxHealth)

    // Life steal heals the attacker
    if c.AttackStrategy == LifeStealStrategy {
        healAmount := damage * 0.3
        c.Heal(healAmount)
        fmt.Printf("%s life steals %.1f HP (HP: %.1f/%.1f)\n",
            c.Name, healAmount, c.Health, c.MaxHealth)
    }
}

func (c *Character) TakeDamage(damage float64) {
    c.Health -= damage
    if c.Health < 0 {
        c.Health = 0
    }
}

func (c *Character) Heal(amount float64) {
    c.Health += amount
    if c.Health > c.MaxHealth {
        c.Health = c.MaxHealth
    }
}

func (c *Character) IsAlive() bool {
    return c.Health > 0
}

func ExampleCombatSystem() {
    fmt.Println("=== Combat System with Strategy Pattern ===\n")

    warrior := NewCharacter("Warrior", 100, 25, 15)
    boss := NewCharacter("Dragon Boss", 300, 30, 20)
    boss.ElementWeakness = true

    // Round 1: Normal attack
    fmt.Println("--- Round 1: Normal Attack ---")
    warrior.AttackTarget(boss)

    // Round 2: Switch to critical attack
    fmt.Println("\n--- Round 2: Critical Strike ---")
    warrior.SetAttackStrategy(CriticalAttackStrategy)
    warrior.AttackTarget(boss)

    // Round 3: Use armor piercing
    fmt.Println("\n--- Round 3: Armor Piercing ---")
    warrior.SetAttackStrategy(ArmorPiercingStrategy)
    warrior.AttackTarget(boss)

    // Round 4: Elemental attack
    fmt.Println("\n--- Round 4: Elemental Attack ---")
    warrior.SetAttackStrategy(ElementalAttackStrategy)
    warrior.AttackTarget(boss)

    // Boss strikes back
    fmt.Println("\n--- Boss Counter-Attack ---")
    boss.AttackTarget(warrior)

    // Round 5: Life steal to recover
    fmt.Println("\n--- Round 5: Life Steal ---")
    warrior.SetAttackStrategy(LifeStealStrategy)
    warrior.AttackTarget(boss)

    fmt.Println("\n=== Combat Complete ===")
}
graph LR Context[Character]:::lightBlue Strategy[AttackStrategy]:::lightYellow Normal[Normal Attack]:::lightGreen Critical[Critical Attack]:::lightGreen ArmorPiercing[Armor Piercing]:::lightGreen Elemental[Elemental]:::lightGreen LifeSteal[Life Steal]:::lightGreen Context -->|Uses| Strategy Strategy -.Can be.-> Normal Strategy -.Can be.-> Critical Strategy -.Can be.-> ArmorPiercing Strategy -.Can be.-> Elemental Strategy -.Can be.-> LifeSteal classDef lightBlue fill:#87CEEB,stroke:#4682B4,stroke-width:2px,color:#000 classDef lightYellow fill:#FFFFE0,stroke:#FFD700,stroke-width:2px,color:#000 classDef lightGreen fill:#90EE90,stroke:#228B22,stroke-width:2px,color:#000

Advanced: Composing Strategies

Strategies can be composed for complex behaviors:

// StrategyComposer combines multiple strategies
func ComposeStrategies(strategies ...AttackStrategy) AttackStrategy {
    return func(baseDamage float64, target *Character) float64 {
        totalDamage := 0.0
        for _, strategy := range strategies {
            totalDamage += strategy(baseDamage, target)
        }
        return totalDamage / float64(len(strategies))
    }
}

// Create a hybrid strategy
hybridStrategy := ComposeStrategies(
    ElementalAttackStrategy,
    ArmorPiercingStrategy,
)

warrior.SetAttackStrategy(hybridStrategy)

Strategy Pattern with Closures

Closures let you create parameterized strategies:

// DamageMultiplierStrategy creates strategies with custom multipliers
func DamageMultiplierStrategy(multiplier float64) AttackStrategy {
    return func(baseDamage float64, target *Character) float64 {
        damage := (baseDamage * multiplier) - target.Defense*0.5
        return math.Max(1, damage)
    }
}

// ChanceBasedStrategy has a probability of applying an effect
func ChanceBasedStrategy(baseStrategy AttackStrategy, chance float64) AttackStrategy {
    return func(baseDamage float64, target *Character) float64 {
        if rand.Float64() < chance {
            fmt.Println("  [Proc!]")
            return baseStrategy(baseDamage, target)
        }
        return NormalAttackStrategy(baseDamage, target)
    }
}

// Usage
berserkerMode := DamageMultiplierStrategy(3.0)
luckyStrike := ChanceBasedStrategy(CriticalAttackStrategy, 0.3)

warrior.SetAttackStrategy(berserkerMode)
warrior.SetAttackStrategy(luckyStrike)

Real-World Example: Sorting Strategies

Classic strategy pattern use case:

package main

import (
    "fmt"
    "sort"
)

type SortStrategy func([]int) []int

func BubbleSort(data []int) []int {
    arr := make([]int, len(data))
    copy(arr, data)

    n := len(arr)
    for i := 0; i < n-1; i++ {
        for j := 0; j < n-i-1; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }
    return arr
}

func QuickSortStrategy(data []int) []int {
    arr := make([]int, len(data))
    copy(arr, data)

    sort.Ints(arr) // Using built-in which is typically quicksort
    return arr
}

func MergeSortStrategy(data []int) []int {
    arr := make([]int, len(data))
    copy(arr, data)

    if len(arr) <= 1 {
        return arr
    }

    mid := len(arr) / 2
    left := MergeSortStrategy(arr[:mid])
    right := MergeSortStrategy(arr[mid:])

    return merge(left, right)
}

func merge(left, right []int) []int {
    result := make([]int, 0, len(left)+len(right))
    i, j := 0, 0

    for i < len(left) && j < len(right) {
        if left[i] <= right[j] {
            result = append(result, left[i])
            i++
        } else {
            result = append(result, right[j])
            j++
        }
    }

    result = append(result, left[i:]...)
    result = append(result, right[j:]...)

    return result
}

type Sorter struct {
    strategy SortStrategy
}

func NewSorter(strategy SortStrategy) *Sorter {
    return &Sorter{strategy: strategy}
}

func (s *Sorter) Sort(data []int) []int {
    return s.strategy(data)
}

func (s *Sorter) SetStrategy(strategy SortStrategy) {
    s.strategy = strategy
}

func ExampleSorting() {
    data := []int{64, 34, 25, 12, 22, 11, 90}
    fmt.Printf("Original: %v\n\n", data)

    sorter := NewSorter(BubbleSort)

    fmt.Println("Bubble Sort:")
    fmt.Printf("Sorted: %v\n\n", sorter.Sort(data))

    sorter.SetStrategy(QuickSortStrategy)
    fmt.Println("Quick Sort:")
    fmt.Printf("Sorted: %v\n\n", sorter.Sort(data))

    sorter.SetStrategy(MergeSortStrategy)
    fmt.Println("Merge Sort:")
    fmt.Printf("Sorted: %v\n", sorter.Sort(data))
}

Strategy Selection Based on Context

Automatically choose strategies based on conditions:

func SelectAttackStrategy(attacker, target *Character) AttackStrategy {
    // Low health? Use life steal
    if attacker.Health < attacker.MaxHealth*0.3 {
        return LifeStealStrategy
    }

    // Enemy has elemental weakness? Use elemental
    if target.ElementWeakness {
        return ElementalAttackStrategy
    }

    // High attack stat? Go for critical
    if attacker.Attack > 30 {
        return CriticalAttackStrategy
    }

    // Default to normal
    return NormalAttackStrategy
}

// Auto-select strategy
warrior.SetAttackStrategy(SelectAttackStrategy(warrior, boss))
warrior.AttackTarget(boss)

Benefits of Strategy Pattern

  1. Flexibility: Swap algorithms at runtime
  2. Open/Closed Principle: Add new strategies without modifying existing code
  3. Testability: Test each strategy independently
  4. Composition: Combine strategies for complex behaviors
  5. Simplicity: No inheritance hierarchies needed in Go

When to Use Strategy Pattern

Strategy pattern shines when:

  • You have multiple algorithms for the same task
  • You need to switch behaviors at runtime
  • You want to eliminate conditional logic
  • You need to support different variations of an algorithm
  • You’re building plugin systems or extensible frameworks

Comparison: OOP vs Functional Strategy

Aspect OOP (Java-style) Go Functions
Verbosity High (interfaces, classes) Low (just functions)
Flexibility Medium High (closures)
Type Safety Strong Strong
Composition Inheritance/delegation Function composition
Boilerplate Significant Minimal

Thank you

The Strategy pattern in Go leverages first-class functions to create elegant, flexible solutions. By treating algorithms as interchangeable values, you build systems that adapt to changing requirements without the complexity of traditional OOP implementations. It’s functional programming at its finest.

Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!