{} The Go Reference

Behavioral pattern · Gang of Four · Intermediate

Template Method

Define the skeleton of an algorithm once, deferring the steps that vary to per-type implementations.

Behavioral Intermediate ⏱ 4 min read Complete

🍵 Analogy

Making a hot drink follows a fixed recipe: boil water, brew, pour into a cup, add condiments. Tea and coffee differ only in how they brew and what condiments they add — the steps and their order never change. Template Method is that recipe card: the skeleton is fixed, two steps are left blank for each drink to fill in.

The problem

Sending a one-time passcode is always the same dance: generate the code, cache it, build a message, send a notification. Only a couple of steps differ between SMS and email. If you copy the whole sequence into each channel, the shared order drifts and bugs creep in. Template Method writes the sequence once and lets each channel supply just the steps that vary.

Structure

classDiagram
class OTPFlow {
  -steps OTPSteps
  +GenAndSend(length) "fixed skeleton"
}
class OTPSteps {
  <<interface>>
  +genOTP(length)
  +saveCache(otp)
  +message(otp)
  +send(msg)
}
class SMS {
  +genOTP() +saveCache() +message() +send()
}
class Email {
  +genOTP() +saveCache() +message() +send()
}
OTPFlow o--> OTPSteps : calls the varying steps
OTPSteps <|.. SMS
OTPSteps <|.. Email

Idiomatic Go

Go has no abstract base class, so the “template method” lives on a struct that holds an interface of the varying steps — an OTP/iOTP-style design. The fixed sequence calls into that interface. Edit and Run:

template_method.go — editable & runnable
package main

import "fmt"

// The steps that vary between channels.
type OTPSteps interface {
genOTP(length int) string
saveCache(otp string)
message(otp string) string
send(msg string) error
}

// Template method: the fixed sequence, written exactly once.
type OTPFlow struct{ steps OTPSteps }

func (f OTPFlow) GenAndSend(length int) error {
otp := f.steps.genOTP(length) // varies
f.steps.saveCache(otp)        // varies
msg := f.steps.message(otp)   // varies
fmt.Println(msg)
return f.steps.send(msg)      // varies
}

type SMS struct{}

func (SMS) genOTP(n int) string       { return "1234" }
func (SMS) saveCache(otp string)      { fmt.Println("SMS OTP cached") }
func (SMS) message(otp string) string { return "Your SMS login code is " + otp }
func (SMS) send(msg string) error     { fmt.Println("sent via SMS"); return nil }

type Email struct{}

func (Email) genOTP(n int) string       { return "5678" }
func (Email) saveCache(otp string)      { fmt.Println("Email OTP cached") }
func (Email) message(otp string) string { return "Your email login code is " + otp }
func (Email) send(msg string) error     { fmt.Println("sent via Email"); return nil }

func main() {
OTPFlow{steps: SMS{}}.GenAndSend(4)
fmt.Println("---")
OTPFlow{steps: Email{}}.GenAndSend(4)
}

🐹 Two Go flavors — interface or function hooks

One version embeds the base OTP in each channel; injecting the steps as an interface field (above) is the cleaner variant. For just one or two varying steps, you can skip the interface entirely and pass them as function fields on the struct — same pattern, less ceremony. Either way, the fixed flow stays in one place.

The Hollywood Principle & hooks

Template Method is inversion of control in its purest form — the Hollywood Principle: “don’t call us, we’ll call you.” You don’t write the algorithm and call helpers; the skeleton runs the show and calls down into your steps at the right moments. That’s exactly how sort.Sort (it calls your Less/Swap) and go test (it calls your TestXxx) work.

The book also distinguishes two kinds of step:

  • Abstract steps — mandatory; every variant must supply them (brew, addCondiments).
  • Hooksoptional steps with a default (often a no-op or a true), which a variant overrides only if it cares (customerWantsCondiments() bool).

In Go you get hooks cheaply with function fields: required steps are funcs every beverage sets, while a hook is a func that defaults to nil and the skeleton treats nil as the default answer. Edit and Run — black coffee overrides the hook to skip condiments, tea leaves it unset and gets the default:

hooks.go — editable & runnable
package main

import "fmt"

// Beverage is the template. Brew/AddCondiments are abstract steps (required);
// WantsCondiments is a HOOK — nil means "use the default" (yes).
type Beverage struct {
Name            string
Brew            func()      // abstract step (required)
AddCondiments   func()      // abstract step (required)
WantsCondiments func() bool // hook (optional)
}

// Prepare is the fixed skeleton — written exactly once.
func (b Beverage) Prepare() {
fmt.Println("--", b.Name, "--")
boilWater()
b.Brew()
pourInCup()
if b.WantsCondiments == nil || b.WantsCondiments() { // default: true
	b.AddCondiments()
}
}

func boilWater() { fmt.Println("boiling water") }
func pourInCup() { fmt.Println("pouring into cup") }

func main() {
tea := Beverage{
	Name:          "Tea",
	Brew:          func() { fmt.Println("steeping the tea") },
	AddCondiments: func() { fmt.Println("adding lemon") },
	// hook unset → condiments added by default
}
blackCoffee := Beverage{
	Name:            "Black Coffee",
	Brew:            func() { fmt.Println("dripping through filter") },
	AddCondiments:   func() { fmt.Println("adding sugar & milk") },
	WantsCondiments: func() bool { return false }, // override the hook
}

tea.Prepare()
blackCoffee.Prepare() // skips condiments via the hook
}

The skeleton (Prepare) stays in one place and owns the order; abstract steps are mandatory function fields; the hook is an optional one the skeleton consults but variants may ignore.

In the standard library

  • sort.Sort — the sorting algorithm is the fixed skeleton; your Len/Less/Swap are the varying steps.
  • text/template execution — a fixed render loop calls your funcs and data.
  • testing — the test runner’s setup → run → teardown flow with your TestXxx body as the variable step.

Pitfalls

⚠️ Watch the line with Strategy

If you find yourself letting callers replace the whole procedure, you’ve drifted into Strategy. Template Method’s value is that it owns the flow and only opens specific holes. If there’s no meaningful fixed skeleton, don’t force one.

When to use it — and when not

✅ Reach for it when

  • Several variants share the same overall procedure but differ in a few steps (SMS vs email OTP, tea vs coffee).
  • You want to write the invariant sequence once and guarantee its order.
  • You want to let callers customize specific steps without touching the orchestration.

⛔ Think twice when

  • There's only one variant — there's no skeleton worth abstracting.
  • The steps don't really share a fixed order; Strategy or plain functions fit better.

Check your understanding

Score: 0 / 5

1. What does Template Method fix, and what does it vary?

The template method holds the invariant sequence; subtypes (or injected step implementations) fill in the parts that differ.

2. Go has no inheritance — how is Template Method expressed idiomatically?

Instead of an abstract base class with overridable hooks, you inject the varying steps as an interface (or as function fields) and the fixed sequence calls into them.

3. How does Template Method differ from Strategy?

Template Method keeps control of the overall flow and lets you fill in blanks; Strategy hands the entire algorithm over to an interchangeable object.

4. Template Method is the textbook case of which principle?

Inversion of control: you don't drive the algorithm and call helpers; the high-level template drives and calls your steps at the right moments. sort.Sort and the testing runner work this way — you provide pieces, the framework calls them.

5. What is a 'hook' in Template Method?

Beyond the mandatory steps, a hook is an optional one with a sensible default — e.g. customerWantsCondiments() returning true by default. Variants override it only when they need to, letting the skeleton offer extension points without forcing every subtype to fill them.

Comments

Sign in with GitHub to join the discussion.