Go

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.

4 min read

Strategy defines a family of algorithms and makes them interchangeable. In Go, the most idiomatic form is a function type — you pass a function value rather than creating an interface with a single method. Use the interface form when the strategy has multiple methods or carries state.

This is the Open/Closed Principle applied to algorithms — the context is open to new behaviours without modifying existing code. It's also one of the patterns that becomes nearly invisible in Go. When someone passes a func to a constructor or a sort.Slice call, they're using Strategy without naming it.

Problem

You need to send notifications through different channels. The current approach switches on a channel name inside the sending function. Every new channel means editing that function, and you can't test one channel's logic without compiling in all the others.

go
func Notify(channel, msg string) error {
    switch channel {
    case "email":
        return sendEmail(msg)
    case "sms":
        return sendSMS(msg)
    case "slack":
        return sendSlack(msg)
    default:
        return fmt.Errorf("unknown channel: %s", channel)
    }
}

The switch is stringly typed. Adding a new channel means modifying Notify. Testing email logic requires the SMS and Slack code to compile too.

Solution

Pull the "how to send" out of Notify and pass it in as a value. In Go, the simplest form is a function type.

text
type NotifyFunc func(msg string) error

Notify(msg, Email)   ──► func(string) error
Notify(msg, SMS)     ──► func(string) error
Notify(msg, Console) ──► func(string) error

The function-type approach — idiomatic Go:

notify.go
package notify

import "fmt"

// NotifyFunc is a strategy for delivering a message.
type NotifyFunc func(msg string) error

func Email(msg string) error {
    fmt.Println("email:", msg)
    return nil
}

func SMS(msg string) error {
    fmt.Println("sms:", msg)
    return nil
}

func Console(msg string) error {
    fmt.Println(msg)
    return nil
}

// Notify runs the strategy. The caller decides how delivery works.
func Notify(msg string, send NotifyFunc) error {
    return send(msg)
}
main.go
notify.Notify("server started", notify.Console)
notify.Notify("order placed",   notify.Email)
notify.Notify("login alert",    notify.SMS)

When a strategy needs configuration or multiple methods, use an interface instead:

notifier.go
package notify

import "fmt"

// Notifier — interface form for strategies that carry state.
type Notifier interface {
    Send(msg string) error
}

type EmailNotifier struct {
    From string
    To   string
}

func (n *EmailNotifier) Send(msg string) error {
    fmt.Printf("[email] %s → %s: %s\n", n.From, n.To, msg)
    return nil
}

type SlackNotifier struct {
    Channel string
}

func (n *SlackNotifier) Send(msg string) error {
    fmt.Printf("[slack] #%s: %s\n", n.Channel, msg)
    return nil
}
go
email := &notify.EmailNotifier{From: "ops@example.com", To: "team@example.com"}
slack := &notify.SlackNotifier{Channel: "alerts"}

email.Send("deploy complete")
slack.Send("deploy complete")

In Go, a function type IS a strategy. sort.Slice(data, func(i, j int) bool { ... }) is Strategy. You don't need an interface for single-method strategies — a func type is simpler and more idiomatic.

When to Use

  • You see a switch or if/else selecting an algorithm based on a type or configuration.
  • The algorithm should be interchangeable at runtime.
  • You want to test business logic independently of the algorithm choice.
  • In Go: if the strategy is a single function, use a function type. If it has state or multiple methods, use an interface.

When Not to Use

  • There's only one algorithm and no expectation of alternatives. Just call the function directly.
  • The algorithms are trivially different. Abstracting them adds ceremony without value.

Tradeoffs

The function-type form costs almost nothing in Go — passing a func is idiomatic and adds no boilerplate. The interface form adds a little more structure but buys you config state and the ability to introspect the strategy (e.g., a Name() method for logging). The cost that never goes away is that the switch doesn't disappear — it moves to the caller. If every call site does if userType == "premium" { send = PremiumNotifier{} }, you've relocated the problem rather than solved it. Centralise strategy selection in a factory or constructor, not scattered across call sites.

  • Bridge — Strategy varies one interchangeable algorithm; Bridge separates two independent dimensions of variation simultaneously — if you have two axes (abstraction + implementation), Bridge; if you have one (algorithm selection), Strategy.
  • State — Both swap behavior at runtime; the distinction is who controls the swap — Strategy is chosen and set by an external caller, State transitions internally in response to events.
  • Template Method — Template Method holds the algorithm skeleton fixed and plugs in one or two steps; Strategy replaces the whole algorithm — prefer Template Method when the structure matters, Strategy when it doesn't.
  • Command — Both encapsulate behavior as a value; Command adds undo and queuing on top — if you need those capabilities, use Command; if you only need interchangeability, Strategy is simpler.