Template Method in Go: Embedding-Based Templates
The Template Method pattern defines an algorithm's skeleton in a base class, deferring specific steps to subclasses. In traditional OOP languages, this relies on inheritance and virtual method...
Key Insights
- Go’s struct embedding provides method promotion but lacks virtual dispatch, meaning embedded methods cannot call “overridden” methods on the outer struct—you must explicitly pass the concrete type as an interface parameter to achieve template method behavior.
- The idiomatic Go approach combines an embedded base struct for shared state and algorithm skeleton with an interface that defines the customizable steps, giving you both code reuse and flexibility.
- Embedding-based templates work best when you have significant shared logic and state; for simpler cases, functional composition with function fields or parameters often produces cleaner, more testable code.
Template Method Without Inheritance
The Template Method pattern defines an algorithm’s skeleton in a base class, deferring specific steps to subclasses. In traditional OOP languages, this relies on inheritance and virtual method dispatch:
// Java: Classic Template Method
abstract class DocumentProcessor {
// Template method - defines the algorithm skeleton
public final void process(String input) {
Document doc = parse(input);
doc = transform(doc);
render(doc);
}
protected abstract Document parse(String input);
protected abstract Document transform(Document doc);
protected abstract void render(Document doc);
}
class MarkdownProcessor extends DocumentProcessor {
@Override
protected Document parse(String input) {
// Markdown-specific parsing
}
// ... other overrides
}
Go doesn’t have inheritance. There’s no extends keyword, no abstract classes, and no virtual method tables. This isn’t a limitation—it’s a deliberate design choice that pushes you toward composition. But it means implementing Template Method requires a different mental model.
The naive approach of embedding a struct and “overriding” its methods simply doesn’t work the way inheritance does. Let’s understand why and build a proper solution.
Go’s Struct Embedding Primer
Struct embedding promotes the embedded type’s methods and fields to the outer struct. This looks like inheritance but behaves differently:
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
func (l *Logger) LogError(err error) {
l.Log("ERROR: " + err.Error()) // Calls Logger.Log
}
type ServiceLogger struct {
*Logger
serviceName string
}
func (s *ServiceLogger) Log(msg string) {
fmt.Printf("[%s/%s] %s\n", s.prefix, s.serviceName, msg)
}
Here’s the critical distinction: when you call serviceLogger.LogError(err), it calls Logger.LogError, which then calls Logger.Log—not ServiceLogger.Log. The embedded Logger has no knowledge of the outer struct. There’s no virtual dispatch.
func main() {
sl := &ServiceLogger{
Logger: &Logger{prefix: "APP"},
serviceName: "auth",
}
sl.Log("direct call") // Output: [APP/auth] direct call
sl.LogError(errors.New("failed")) // Output: [APP] ERROR: failed
// LogError calls Logger.Log, not ServiceLogger.Log!
}
This behavior breaks the classic Template Method pattern where the skeleton method calls virtual methods that subclasses override.
Implementing Template Method via Embedding
The solution is to separate the algorithm skeleton from the customizable steps using an explicit interface. The base struct holds shared state and the template method, but it receives the customizable behavior through an interface parameter:
// Steps interface defines the customizable parts
type ProcessorSteps interface {
Parse(input string) (*Document, error)
Transform(doc *Document) (*Document, error)
Render(doc *Document) ([]byte, error)
}
// Base provides shared functionality and the algorithm skeleton
type Base struct {
Metrics *MetricsCollector
Logger *slog.Logger
MaxRetries int
}
func (b *Base) Process(steps ProcessorSteps, input string) ([]byte, error) {
start := time.Now()
defer func() {
b.Metrics.RecordDuration("process", time.Since(start))
}()
b.Logger.Info("starting document processing")
// Step 1: Parse
doc, err := steps.Parse(input)
if err != nil {
b.Logger.Error("parse failed", "error", err)
return nil, fmt.Errorf("parse: %w", err)
}
// Step 2: Transform
doc, err = steps.Transform(doc)
if err != nil {
b.Logger.Error("transform failed", "error", err)
return nil, fmt.Errorf("transform: %w", err)
}
// Step 3: Render
output, err := steps.Render(doc)
if err != nil {
b.Logger.Error("render failed", "error", err)
return nil, fmt.Errorf("render: %w", err)
}
b.Logger.Info("processing complete", "output_size", len(output))
return output, nil
}
Now concrete processors embed Base for shared functionality and implement ProcessorSteps:
type MarkdownProcessor struct {
*Base
HTMLTemplate *template.Template
}
func (m *MarkdownProcessor) Parse(input string) (*Document, error) {
// Markdown-specific parsing using goldmark, blackfriday, etc.
ast := markdown.Parse([]byte(input))
return &Document{AST: ast, Format: "markdown"}, nil
}
func (m *MarkdownProcessor) Transform(doc *Document) (*Document, error) {
// Apply transformations: syntax highlighting, link rewriting, etc.
doc.AST = transformAST(doc.AST)
return doc, nil
}
func (m *MarkdownProcessor) Render(doc *Document) ([]byte, error) {
var buf bytes.Buffer
if err := markdown.Render(&buf, doc.AST); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ProcessDocument uses the template method with itself as steps
func (m *MarkdownProcessor) ProcessDocument(input string) ([]byte, error) {
return m.Base.Process(m, input) // Pass self as the steps interface
}
The key insight is m.Base.Process(m, input)—we pass the concrete type as the interface parameter, enabling the base’s algorithm to call back into the concrete implementation.
Handling the “Self” Problem
The pattern above solves what I call the “self problem”—the inability of embedded methods to reference the embedding struct. Let’s make this more explicit with a cleaner API:
// Template defines both the skeleton and the contract
type Template[T any] struct {
steps Steps[T]
hooks Hooks
}
type Steps[T any] interface {
Initialize() (T, error)
Execute(state T) (T, error)
Finalize(state T) error
}
type Hooks struct {
BeforeExecute func() error
AfterExecute func() error
}
func NewTemplate[T any](steps Steps[T], hooks Hooks) *Template[T] {
return &Template[T]{steps: steps, hooks: hooks}
}
func (t *Template[T]) Run() error {
state, err := t.steps.Initialize()
if err != nil {
return fmt.Errorf("initialize: %w", err)
}
if t.hooks.BeforeExecute != nil {
if err := t.hooks.BeforeExecute(); err != nil {
return fmt.Errorf("before hook: %w", err)
}
}
state, err = t.steps.Execute(state)
if err != nil {
return fmt.Errorf("execute: %w", err)
}
if t.hooks.AfterExecute != nil {
if err := t.hooks.AfterExecute(); err != nil {
return fmt.Errorf("after hook: %w", err)
}
}
return t.steps.Finalize(state)
}
This generic template accepts any type implementing Steps[T] and optional hooks. The concrete implementation owns the algorithm’s customizable behavior while the template owns the structure.
Real-World Example: HTTP Handler Pipeline
Let’s build a practical REST API handler template with authentication, validation, and response formatting:
type HandlerSteps[Req, Resp any] interface {
Authenticate(r *http.Request) (UserContext, error)
ParseRequest(r *http.Request) (Req, error)
Validate(req Req) error
Execute(ctx context.Context, req Req) (Resp, error)
FormatResponse(resp Resp) ([]byte, int, error)
}
type HandlerBase struct {
Logger *slog.Logger
Metrics *MetricsCollector
}
func (h *HandlerBase) ServeHTTP[Req, Resp any](
steps HandlerSteps[Req, Resp],
w http.ResponseWriter,
r *http.Request,
) {
start := time.Now()
// Step 1: Authenticate
userCtx, err := steps.Authenticate(r)
if err != nil {
h.writeError(w, http.StatusUnauthorized, "authentication failed")
return
}
ctx := context.WithValue(r.Context(), userContextKey, userCtx)
// Step 2: Parse request
req, err := steps.ParseRequest(r)
if err != nil {
h.writeError(w, http.StatusBadRequest, "invalid request format")
return
}
// Step 3: Validate
if err := steps.Validate(req); err != nil {
h.writeError(w, http.StatusBadRequest, err.Error())
return
}
// Step 4: Execute business logic
resp, err := steps.Execute(ctx, req)
if err != nil {
h.handleExecuteError(w, err)
return
}
// Step 5: Format and write response
body, status, err := steps.FormatResponse(resp)
if err != nil {
h.writeError(w, http.StatusInternalServerError, "response formatting failed")
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(body)
h.Metrics.RecordDuration("handler", time.Since(start))
}
func (h *HandlerBase) writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
A concrete handler embeds HandlerBase and implements the steps:
type CreateUserHandler struct {
*HandlerBase
userService *UserService
}
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
}
func (h *CreateUserHandler) Authenticate(r *http.Request) (UserContext, error) {
token := r.Header.Get("Authorization")
return h.userService.ValidateAdminToken(token)
}
func (h *CreateUserHandler) ParseRequest(r *http.Request) (CreateUserRequest, error) {
var req CreateUserRequest
err := json.NewDecoder(r.Body).Decode(&req)
return req, err
}
func (h *CreateUserHandler) Validate(req CreateUserRequest) error {
if req.Email == "" {
return errors.New("email is required")
}
return nil
}
func (h *CreateUserHandler) Execute(ctx context.Context, req CreateUserRequest) (CreateUserResponse, error) {
user, err := h.userService.Create(ctx, req.Email, req.Name)
if err != nil {
return CreateUserResponse{}, err
}
return CreateUserResponse{ID: user.ID, Email: user.Email}, nil
}
func (h *CreateUserHandler) FormatResponse(resp CreateUserResponse) ([]byte, int, error) {
body, err := json.Marshal(resp)
return body, http.StatusCreated, err
}
func (h *CreateUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.HandlerBase.ServeHTTP(h, w, r)
}
Trade-offs and Alternative Approaches
Embedding-based templates shine when you have substantial shared state and logic. But for simpler cases, functional composition is often cleaner:
// Functional approach - same template, no embedding required
type ProcessFunc func(input string) ([]byte, error)
func NewProcessor(
parse func(string) (*Document, error),
transform func(*Document) (*Document, error),
render func(*Document) ([]byte, error),
logger *slog.Logger,
) ProcessFunc {
return func(input string) ([]byte, error) {
logger.Info("starting processing")
doc, err := parse(input)
if err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
doc, err = transform(doc)
if err != nil {
return nil, fmt.Errorf("transform: %w", err)
}
return render(doc)
}
}
// Usage - compose functions without structs
processor := NewProcessor(
parseMarkdown,
applyTransforms,
renderHTML,
logger,
)
Use embedding-based templates when:
- You have significant shared state across implementations
- The customizable steps need access to shared resources
- You want a clear type hierarchy for documentation and tooling
Use functional composition when:
- Steps are stateless or have minimal shared state
- You want maximum flexibility in composing behaviors
- Testing individual steps in isolation is a priority
Idiomatic Go Patterns
The Template Method pattern translates to Go through explicit interface parameters rather than implicit virtual dispatch. This approach is more verbose than inheritance-based implementations but offers clearer contracts and easier testing.
Prefer embedding when you genuinely need shared state and behavior. Don’t use it just to mimic inheritance—Go’s interfaces and functions often provide cleaner solutions. When you do use embedding-based templates, always pass the concrete type explicitly to the algorithm skeleton. This makes the control flow obvious and avoids the confusion that comes from expecting inheritance-like behavior from composition.