I. Strategy — Behavioral Pattern
Strategy defines a family of interchangeable algorithms, encapsulates each one behind a common interface, and lets the client swap between them at runtime. The object that uses the algorithm — the Context — never hard-codes which one it's running; it just delegates to whichever strategy it currently holds.
This solves the problem of algorithm-specific branching creeping into business logic. Without Strategy, adding a new pricing rule usually means adding another if/switch branch inside the checkout code itself, which grows harder to test and more error-prone with every new rule. Strategy moves each algorithm into its own type, so the checkout code stays a single, unchanging delegation call.
It's the same shape as sorting with a custom comparator, or picking a compression algorithm at runtime — the caller stays generic, the algorithm is swappable.
II. Real-world Example
A coffee shop checkout where the pricing rule depends on the type of customer:
- Checkout (Context) — holds a
PricingStrategyand exposesTotal(basePrice)andSetStrategy(strategy). It has no idea what "Happy Hour" or "VIP" actually means.
- PricingStrategy (interface) — declares
Calculate(basePrice float64) float64andName() string, the contract every pricing rule must implement.
- RegularPricing — charges full price.
- HappyHourPricing — applies a 20% discount.
- VIPPricing — applies a 30% discount.
Swapping strategies at runtime via SetStrategy changes Checkout's behavior without touching its code.

III. Implementation
pricing_strategy.go
package strategy
type PricingStrategy interface {
Calculate(basePrice float64) float64
Name() string
}checkout.go
package strategy
type Checkout struct {
strategy PricingStrategy
}
func NewCheckout(strategy PricingStrategy) *Checkout {
return &Checkout{strategy: strategy}
}
func (c *Checkout) SetStrategy(strategy PricingStrategy) {
c.strategy = strategy
}
func (c *Checkout) Total(basePrice float64) float64 {
return c.strategy.Calculate(basePrice)
}
func (c *Checkout) StrategyName() string {
return c.strategy.Name()
}regular_pricing.go
package strategy
type RegularPricing struct{}
func (RegularPricing) Calculate(basePrice float64) float64 {
return basePrice
}
func (RegularPricing) Name() string {
return "Regular"
}happy_hour_pricing.go
package strategy
type HappyHourPricing struct{}
func (HappyHourPricing) Calculate(basePrice float64) float64 {
return basePrice * 0.8
}
func (HappyHourPricing) Name() string {
return "Happy Hour"
}vip_pricing.go
package strategy
type VIPPricing struct{}
func (VIPPricing) Calculate(basePrice float64) float64 {
return basePrice * 0.7
}
func (VIPPricing) Name() string {
return "VIP"
}main.go (usage)
checkout := strategy.NewCheckout(strategy.RegularPricing{})
basePrice := 5.0
fmt.Printf("Regular total: %.2f (%s)\n", checkout.Total(basePrice), checkout.StrategyName())
checkout.SetStrategy(strategy.HappyHourPricing{})
fmt.Printf("Happy hour total: %.2f (%s)\n", checkout.Total(basePrice), checkout.StrategyName())
checkout.SetStrategy(strategy.VIPPricing{})
fmt.Printf("VIP total: %.2f (%s)\n", checkout.Total(basePrice), checkout.StrategyName())IV. Explain the example above
strategy.NewCheckout(strategy.RegularPricing{})creates a Checkout whose current strategy isRegularPricing.
checkout.Total(5.0)delegates toRegularPricing.Calculate(5.0), which simply returns5.0unchanged.
checkout.SetStrategy(strategy.HappyHourPricing{})swaps the internal strategy pointer — Checkout's own code doesn't change at all.
checkout.Total(5.0)now delegates toHappyHourPricing.Calculate(5.0), returning5.0 * 0.8 = 4.0.
checkout.SetStrategy(strategy.VIPPricing{})swaps again, andcheckout.Total(5.0)now returns5.0 * 0.7 = 3.5.
Running it produces:
*** Example Strategy ***
Regular total: 5.00 (Regular)
Happy hour total: 4.00 (Happy Hour)
VIP total: 3.50 (VIP)
*** End of Strategy ***The same basePrice produces three different totals, and Checkout.Total never branches on which pricing rule is active — it just calls c.strategy.Calculate(basePrice). Adding a new pricing tier means adding a new type that implements PricingStrategy, not editing Checkout.
V. Conclusion
Strategy is a good fit whenever a piece of behavior has multiple valid implementations that need to be selected or swapped at runtime, and you want that selection logic kept out of the code that uses the behavior. It keeps the Context small and stable while the family of algorithms can grow independently.
The trade-off is one more layer of indirection and one more type per algorithm — for two straightforward variants that never change, an if/else inside the Context is often more readable than a full interface plus multiple structs. Strategy earns its keep once the number of variants grows, or when the choice of algorithm needs to be injected from outside (config, user tier, feature flag) rather than hard-coded.
No pattern is universally superior.
VI. References
- Go Design Patterns (Mario Castro Contreras)




