Strategy Pattern — Junior¶
1. What the Strategy pattern actually is¶
You have an operation whose core is fixed, but one specific step varies. Sorting always compares-and-swaps, but the comparison rule depends on the data. Compression always reads-bytes-and-writes-bytes, but the algorithm could be gzip, zstd, or LZ4. Payment processing always validates-and-charges, but the gateway is Stripe, PayPal, or in-house.
The Strategy pattern names this: lift the varying step into its own type (in Go, almost always an interface or a function value), pass it as a parameter, and let the caller pick at runtime which version runs.
// The varying step is sorting's comparator.
sort.Slice(users, func(i, j int) bool { return users[i].Age < users[j].Age })
// Now sort by name instead — same operation, different strategy.
sort.Slice(users, func(i, j int) bool { return users[i].Name < users[j].Name })
sort.Slice is a strategy-using function. The strategy here is the func(i, j int) bool you pass in. Everything else — partitioning, recursion, swapping — stays the same.
This file teaches:
- The two Go shapes (interface vs first-class function) and when to pick which.
- Why Strategy is the most common GoF pattern in Go — usually invisibly, via single-method interfaces.
- How it differs from State, Template Method, and Command (often confused).
If you've used io.Writer, http.Handler, or sort.Interface, you've already used Strategy. The pattern has no name in Go because it's the default way of doing things.
2. Table of Contents¶
- What the Strategy pattern actually is
- Table of Contents
- The two Go shapes
- Minimum implementation — interface variant
- Minimum implementation — function variant
- Choosing between interface and function
- Strategy vs polymorphism — the same thing
- Strategy in the standard library
- A second worked example: discount calculation
- Strategy vs Template Method vs State vs Command
- Common mistakes a junior makes
- Tricky points
- Quick test
- Cheat sheet
- What to learn next
3. The two Go shapes¶
The GoF book described Strategy in terms of classes and inheritance. Go has neither, but the pattern survives in two clean shapes:
| Shape | Definition | When to pick |
|---|---|---|
| Interface | type Sorter interface { Sort([]int) } | Strategy has multiple methods, or needs state, or is published across packages |
| First-class function | type Less func(i, j int) bool | Strategy is a single operation, no state, simple signature |
Both are "Strategy" in the GoF sense — a separate type whose value can be swapped at runtime. The Go community uses the function variant whenever it fits because it's less code, less indirection, and equally idiomatic.
Most "strategy" decisions in real Go code take 30 seconds: a func if the signature is simple, an interface otherwise.
4. Minimum implementation — interface variant¶
A payment processor that delegates the actual charge to a gateway-specific implementation.
package payment
import (
"context"
"fmt"
)
// Gateway is the strategy. Each implementation handles one payment provider.
type Gateway interface {
Charge(ctx context.Context, amountCents int, currency string) (chargeID string, err error)
}
// Processor uses a Gateway. The processor's logic is the same regardless of provider.
type Processor struct {
gateway Gateway
}
func NewProcessor(g Gateway) *Processor {
return &Processor{gateway: g}
}
func (p *Processor) Process(ctx context.Context, order Order) (string, error) {
if order.AmountCents <= 0 {
return "", fmt.Errorf("Process: zero/negative amount")
}
id, err := p.gateway.Charge(ctx, order.AmountCents, order.Currency)
if err != nil {
return "", fmt.Errorf("Process: %w", err)
}
// ... post-charge bookkeeping, the same for every gateway ...
return id, nil
}
// Two concrete strategies:
type StripeGateway struct{ apiKey string }
func (s *StripeGateway) Charge(ctx context.Context, amount int, ccy string) (string, error) {
// call Stripe's API
return "stripe_ch_123", nil
}
type PayPalGateway struct{ clientID, secret string }
func (p *PayPalGateway) Charge(ctx context.Context, amount int, ccy string) (string, error) {
// call PayPal's API
return "pp_xyz", nil
}
Usage:
p1 := payment.NewProcessor(&payment.StripeGateway{apiKey: "..."})
id, err := p1.Process(ctx, order)
p2 := payment.NewProcessor(&payment.PayPalGateway{clientID: "...", secret: "..."})
id2, err := p2.Process(ctx, order)
The Processor.Process logic is identical in both cases. Only the gateway differs. That's Strategy.
What makes this idiomatic Go¶
- The interface (
Gateway) is small — one method. Go's culture is "small interfaces". AChargemethod is enough; don't preemptively addRefund,Capture, etc. unless the processor actually uses them. - The interface is declared where it's used (the
paymentpackage), not where the implementations live. Stripe and PayPal SDKs don't importpayment; they just happen to satisfy the interface. - Concrete types are exported (
StripeGateway) but the consumer types accept the interface. Accept interfaces, return concrete types.
5. Minimum implementation — function variant¶
Same problem with a single-method interface, written as a function instead.
package payment
import (
"context"
"fmt"
)
// ChargeFunc is the strategy as a function value.
type ChargeFunc func(ctx context.Context, amountCents int, currency string) (string, error)
type Processor struct {
charge ChargeFunc
}
func NewProcessor(c ChargeFunc) *Processor { return &Processor{charge: c} }
func (p *Processor) Process(ctx context.Context, order Order) (string, error) {
if order.AmountCents <= 0 {
return "", fmt.Errorf("Process: zero/negative amount")
}
return p.charge(ctx, order.AmountCents, order.Currency)
}
Strategy values are now just functions:
stripeCharge := func(ctx context.Context, amount int, ccy string) (string, error) {
// call Stripe
return "stripe_ch_123", nil
}
p := payment.NewProcessor(stripeCharge)
id, _ := p.Process(ctx, order)
Or via a closure that captures configuration:
func NewStripeCharge(apiKey string) payment.ChargeFunc {
return func(ctx context.Context, amount int, ccy string) (string, error) {
// use apiKey to call Stripe
return "stripe_ch_123", nil
}
}
p := payment.NewProcessor(NewStripeCharge("sk_test_..."))
This is shorter than the interface variant. No type declarations for each gateway, no method receiver boilerplate. The closure captures the API key the same way the StripeGateway struct held it.
When the function variant is enough¶
If your strategy is one operation with a fixed signature, the function variant is almost always shorter and clearer. The interface variant earns its keep when:
- The strategy has multiple operations (e.g.,
Charge+Refund+Capture). - The strategy needs to expose state or introspection (e.g., "give me the gateway name").
- You expect downstream packages to define many implementations, and a named interface improves documentation and IDE navigation.
For a single function with a clean signature, just use the function.
6. Choosing between interface and function¶
A more concrete decision table than §3.
| Signal | Lean toward |
|---|---|
| One method, fixed args | Function |
| Implementation needs config that varies per instance | Either (closure captures config; struct holds config) |
| Two or more related methods | Interface |
| The strategy is a value with identity (e.g., the gateway has a Name(), an ID()) | Interface |
| Callers want to mock for tests | Either — both mock easily |
| Hot path with millions of calls | Function (one fewer interface dispatch) |
| External authors will write implementations | Interface (documented contract, easier to discover) |
| The strategy may need to evolve to multiple methods | Interface (so adding a method doesn't break the contract type) |
Don't over-think this. Most real codebases mix both shapes and that's fine — pick what reads best at the call site.
7. Strategy vs polymorphism — the same thing¶
In a Java or C++ textbook, "Strategy" is a named pattern: an interface, several implementing classes, a context object that holds a reference and delegates. In Go, the same thing happens any time you accept an interface and call its method. There's no separate "Strategy" file or naming convention — it's just how Go works.
// This is Strategy. There's no need to label it.
func WriteAll(w io.Writer, data []byte) error {
_, err := w.Write(data)
return err
}
The io.Writer is the strategy interface. *os.File, *bytes.Buffer, *gzip.Writer are concrete strategies. WriteAll is the context. The pattern is in the bones of the standard library.
Knowing the name is useful when:
- Reading non-Go code and translating concepts (a Java codebase's
XxxStrategyinterface is almost always Go'sXxxinterface). - Discussing design with developers from non-Go backgrounds.
- Recognising when a piece of Go code is implementing Strategy and could be simplified by using a
functype instead.
But you don't need to name anything Strategy in Go code. Naming an interface PaymentStrategy is a smell — name it Gateway (or Charger, or just Payment).
8. Strategy in the standard library¶
You've used Strategy hundreds of times in stdlib without thinking about it:
| Where | Strategy type | What varies |
|---|---|---|
sort.Slice / sort.Sort | func(i, j int) bool / sort.Interface | Comparison rule |
io.Copy | io.Reader and io.Writer | Source and destination |
http.Handle | http.Handler (interface) or http.HandlerFunc (function) | Request handling |
json.Marshaler / json.Unmarshaler | Interface | Custom serialization |
flag.Var | flag.Value interface | Custom flag parsing |
errors.Is / errors.As | Sentinel error or target | Error matching |
runtime.SetFinalizer | interface{} plus finalizer func | Cleanup behaviour |
template.Funcs | template.FuncMap (map of strategies) | Template function calls |
Look at http.HandlerFunc:
// from net/http
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
That's the trick for getting "interface or function" at the same call site: define both. HandlerFunc is a type with a method that satisfies Handler, but instances of HandlerFunc are just functions. Callers can pass either:
http.Handle("/api", &MyHandler{}) // interface form
http.Handle("/api", http.HandlerFunc(handleAPI)) // function form
http.HandleFunc("/api", handleAPI) // shortcut
This is the canonical Go strategy idiom. We come back to it in detail in middle.md §6.
9. A second worked example: discount calculation¶
A checkout that supports different discount strategies — flat off, percentage, BOGO.
package checkout
// Discount is the strategy.
type Discount interface {
Apply(subtotalCents int) (discountCents int)
}
type Cart struct {
items []Item
discounts []Discount
}
func (c *Cart) Total() int {
sub := c.subtotal()
var total = sub
for _, d := range c.discounts {
total -= d.Apply(total) // each discount applies to the running total
}
if total < 0 {
total = 0
}
return total
}
func (c *Cart) subtotal() int {
sub := 0
for _, it := range c.items {
sub += it.PriceCents * it.Quantity
}
return sub
}
// Three concrete strategies:
type FlatDiscount struct{ AmountCents int }
func (d FlatDiscount) Apply(_ int) int { return d.AmountCents }
type PercentDiscount struct{ Percent float64 }
func (d PercentDiscount) Apply(subtotal int) int {
return int(float64(subtotal) * d.Percent / 100)
}
type CodeDiscount struct {
Code string
AmountCents int
}
func (d CodeDiscount) Apply(_ int) int { return d.AmountCents }
Usage:
cart := &Cart{
items: []Item{{PriceCents: 1000, Quantity: 2}},
discounts: []Discount{
PercentDiscount{Percent: 10}, // 10% off
FlatDiscount{AmountCents: 100}, // then $1 off
},
}
fmt.Println(cart.Total()) // 1700: 2000 - 200 (10%) - 100 (flat)
Three things to notice:
- The cart doesn't know about discount types. Adding a new discount means writing a new struct that implements
Discount. The cart's code doesn't change. - Discounts compose. A
[]Discountis a list of strategies applied in order. We didn't need a "composite discount" type; the slice is the composition. - Each discount is small.
FlatDiscount{AmountCents: 100}is the entire strategy. No constructor, no setup. That's what good Go strategies look like.
10. Strategy vs Template Method vs State vs Command¶
Four GoF patterns that beginners mix up. The differences matter once you've seen each in practice.
Strategy¶
The whole step is replaced. Caller chooses which implementation runs.
Template Method¶
In Go, Template Method is rare because we don't have inheritance. The closest equivalent is a struct with several function fields the caller sets:
type Sorter struct {
Less func(a, b Item) bool
OnSwap func(i, j int) // optional hook
OnFinal func([]Item) // optional hook
}
But this is just multiple strategies in one struct. Go doesn't really separate Template Method from Strategy.
State¶
State is Strategy plus the rule that the strategy changes itself. A traffic light's Tick() advances to the next state; a TCP connection's Read() behaves differently when in Closed vs Established. The strategy is the state; transitions are part of the pattern.
We cover State separately in ../14-state-pattern/.
Command¶
Command bundles a function and its parameters into a value you can store. Strategy is a function; Command is a function-with-args ready to execute later. Command is about when an operation runs; Strategy is about which operation runs.
// Command — encapsulates the call
type Cmd interface { Execute() error }
type SendEmailCmd struct{ To, Subject string }
func (c SendEmailCmd) Execute() error { /* sends */ }
queue := []Cmd{SendEmailCmd{To: "alice@..."}, SendEmailCmd{To: "bob@..."}}
for _, c := range queue { c.Execute() }
We cover Command in ../13-command-pattern/.
Decision table¶
| You need to… | Pattern |
|---|---|
| Swap one step of an algorithm | Strategy |
| Define a skeleton with overridable sub-steps | Template Method |
| Change behaviour based on internal state | State |
| Encapsulate a call (with args) as a queueable object | Command |
In Go, Strategy is the default. The others are special cases.
11. Common mistakes a junior makes¶
11.1 Naming the interface "XxxStrategy"¶
// Smell
type PaymentStrategy interface { Charge(...) }
// Idiomatic
type Gateway interface { Charge(...) }
In Go, the role the interface plays is its name. io.Reader is "something to read from", not "ReaderStrategy". Gateway says what the thing is; PaymentStrategy says what pattern it implements (and the caller doesn't care).
11.2 Designing a big interface upfront¶
// Anti-idiom
type Gateway interface {
Charge(...) error
Refund(...) error
Capture(...) error
Authorize(...) error
GetTransaction(...) error
ListTransactions(...) error
/* 8 more methods */
}
Now every gateway implementation must implement 8 methods, even if your processor only ever calls Charge. Start with the methods you actually use. Add to the interface when a real caller needs it.
// Better
type Charger interface { Charge(...) error }
type Refunder interface { Refund(...) error }
// Implementations may satisfy one or both, depending on capability
This is interface segregation — Go's most under-appreciated principle.
11.3 Returning the strategy interface from a constructor¶
// Anti-idiom
func NewStripeGateway(...) Gateway { return &StripeGateway{} }
// Idiomatic
func NewStripeGateway(...) *StripeGateway { return &StripeGateway{} }
The constructor returns the concrete type. The consumer (the Processor) accepts the interface. This way, callers can use Stripe-specific methods (StripeGateway.SetWebhookSecret(...)) without type assertions, and still pass the same instance into anything expecting a Gateway.
11.4 Mixing function and interface forms inconsistently¶
// Anti-idiom — accept a function in one place, an interface in another, for the same role
func ProcessOne(g Gateway, o Order) error { /* ... */ }
func ProcessAll(c ChargeFunc, os []Order) error { /* ... */ }
The caller now juggles two adapters. Pick one shape per role. If you genuinely need both, define both and the adapter (like http.HandlerFunc).
12. Tricky points¶
12.1 nil strategy¶
Constructors should either reject nil or provide a default. Most production code rejects:
func NewProcessor(g Gateway) (*Processor, error) {
if g == nil {
return nil, errors.New("NewProcessor: gateway is nil")
}
return &Processor{gateway: g}, nil
}
12.2 The "interface vs *T" trap¶
var g Gateway = &StripeGateway{}
if g == nil { fmt.Println("nil") } // doesn't print, even if the concrete pointer is nil
var sg *StripeGateway // nil pointer
g = sg // interface holding a (nil, *StripeGateway) pair
if g == nil { /* won't fire */ }
g.Charge(ctx, 100, "USD") // nil pointer dereference in the method body
An interface is (type, value). Assigning a typed nil to an interface gives you a non-nil interface that wraps a nil. Test interfaces with care; we cover the rules in middle.md §10.
12.3 Closure capture in a loop¶
gateways := []string{"stripe", "paypal", "square"}
processors := make([]*Processor, 0, len(gateways))
for _, g := range gateways {
processors = append(processors, NewProcessor(func() ChargeFunc {
// before Go 1.22: the closure captures the *variable* `g`,
// so every closure sees the last value ("square")
return func(ctx context.Context, a int, c string) (string, error) {
return g + "_ch", nil
}
}()))
}
In Go 1.22+ the for loop variable is per-iteration, so this works. In Go 1.21 and earlier, you had to manually rebind: g := g. Worth knowing because plenty of pre-1.22 codebases still exist.
12.4 Strategy with side effects in the constructor¶
// Anti-idiom
type StripeGateway struct{ client *http.Client }
func NewStripeGateway(apiKey string) *StripeGateway {
sg := &StripeGateway{client: &http.Client{}}
sg.checkAuth(apiKey) // blocking network call in constructor
return sg
}
Constructors that do I/O are surprising. They block, they can fail, and tests have to stub the network. Defer the work to first use or to an explicit Start(ctx) method.
13. Quick test¶
Q1. Which is more idiomatic Go?
Answer
`Sorter`. The Go convention names the interface after the *role* (what it does), not the pattern it implements. `io.Reader` not `ReaderStrategy`. `http.Handler` not `HandlerStrategy`.Q2. What's wrong here?
type Gateway interface {
Charge(ctx context.Context, amount int) error
Refund(ctx context.Context, id string) error
Cancel(ctx context.Context, id string) error
GetStatus(ctx context.Context, id string) (string, error)
GetTransactions(ctx context.Context) ([]Transaction, error)
}
func (p *Processor) Process(o Order) error {
_, err := p.gateway.Charge(context.Background(), o.AmountCents)
return err
}
Answer
The `Processor` only calls `Charge`. The interface is too wide — every implementor has to support five methods even though the processor uses one. Split it: This is *interface segregation*. Strategies should be the minimum surface the consumer needs.Q3. Function or interface — which would you use?
Answer
Function — a `Delimiter func(rune) bool` (returns true if the rune is a delimiter). It's one operation, fixed signature, no state. The interface variant would be needless ceremony. If the rule grew to "handle quoted fields, escapes, multi-line strings" — multiple operations — an interface makes more sense. Start with the function; promote to interface when complexity demands it.14. Cheat sheet¶
| What | How |
|---|---|
| Single-method, simple signature | type X func(args) result |
| Multi-method strategy | type X interface { Method1(...); Method2(...) } |
| Naming | After the role (Reader, Gateway), not the pattern (ReaderStrategy) |
| Interface size | Smallest that satisfies the consumer; segregate when consumers diverge |
| Interface location | Same package as the consumer, not the implementation |
| Constructor return type | Concrete type for implementations, interface for consumer parameters |
| Default strategy | Provide one (constructor sets it if nil), or reject nil with error |
| Combining strategies | []Strategy and a loop — no "composite strategy" type needed |
| Adapter between forms | type FuncStrategy func(...) result with a method that implements the interface (see http.HandlerFunc) |
15. What to learn next¶
In order:
- middle.md — Strategy registries, named strategy lookup, dynamic dispatch costs, the
func + interfaceadapter trick, strategy composition, testability and mocking strategies, generic strategies. - ../04-decorator-pattern/ — When you wrap a strategy with cross-cutting concerns (logging, retries, caching). Decorator wraps Strategy.
- ../01-functional-options/ — Options for configuring a thing; Strategy for replacing one step of a thing. Related, distinct.
Strategy is the most useful and the most invisible pattern in Go. Recognising it makes you write smaller interfaces and resist the urge to declare types like PaymentStrategy. Once it's internalised, you'll see it everywhere — and that's the right way to feel about it.