Files
juanatsap d95c62bad4 refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
2025-12-02 20:25:05 +00:00

14 KiB

Template Pattern in Go

Pattern Overview

The Template Pattern (not to be confused with Go's html/template package) defines the skeleton of an algorithm in a method, deferring some steps to subclasses or functions. In Go, this is often implemented through interfaces and composition rather than inheritance.

In this project's context, we also use Go's template system which provides a different kind of template pattern for rendering HTML.

Pattern Structure

// Abstract template algorithm
type Processor interface {
    Process() error
    Validate() error
    Transform() error
    Save() error
}

// Concrete implementation
type DataProcessor struct {
    // fields
}

func (p *DataProcessor) Process() error {
    // Template method defines the algorithm
    if err := p.Validate(); err != nil {
        return err
    }
    if err := p.Transform(); err != nil {
        return err
    }
    return p.Save()
}

// Steps can be customized
func (p *DataProcessor) Validate() error {
    // Custom validation
}

Real Implementation: Template Manager

Template Manager Structure

// internal/templates/manager.go

// Manager handles template rendering
type Manager struct {
    templates map[string]*template.Template
    config    *config.TemplateConfig
    mu        sync.RWMutex
}

// NewManager creates and initializes template manager
func NewManager(config *config.TemplateConfig) (*Manager, error) {
    m := &Manager{
        templates: make(map[string]*template.Template),
        config:    config,
    }

    // Load templates on initialization
    if err := m.loadTemplates(); err != nil {
        return nil, err
    }

    return m, nil
}

Template Loading Algorithm

// loadTemplates follows a template algorithm pattern
func (m *Manager) loadTemplates() error {
    // Step 1: Find template files
    files, err := filepath.Glob(m.config.Dir + "/*.html")
    if err != nil {
        return fmt.Errorf("glob templates: %w", err)
    }

    // Step 2: For each template file
    for _, file := range files {
        name := filepath.Base(file)

        // Step 3: Create new template
        tmpl := template.New(name)

        // Step 4: Add custom functions
        tmpl = tmpl.Funcs(m.customFunctions())

        // Step 5: Parse main template
        tmpl, err = tmpl.ParseFiles(file)
        if err != nil {
            return fmt.Errorf("parse template %s: %w", name, err)
        }

        // Step 6: Parse partials
        partialsPattern := filepath.Join(m.config.PartialsDir, "*.html")
        tmpl, err = tmpl.ParseGlob(partialsPattern)
        if err != nil {
            return fmt.Errorf("parse partials: %w", err)
        }

        // Step 7: Cache template
        m.templates[name] = tmpl
    }

    log.Printf("Loaded %d templates", len(m.templates))
    return nil
}

Template Rendering Algorithm

// Render follows a consistent algorithm for all templates
func (m *Manager) Render(w io.Writer, name string, data interface{}) error {
    // Step 1: Acquire read lock
    m.mu.RLock()
    defer m.mu.RUnlock()

    // Step 2: Hot reload check (development)
    if m.config.HotReload {
        // Temporarily upgrade to write lock
        m.mu.RUnlock()
        m.mu.Lock()
        m.loadTemplates()  // Reload templates
        m.mu.Unlock()
        m.mu.RLock()
    }

    // Step 3: Get template from cache
    tmpl, ok := m.templates[name]
    if !ok {
        return fmt.Errorf("template not found: %s", name)
    }

    // Step 4: Execute template
    err := tmpl.Execute(w, data)
    if err != nil {
        return fmt.Errorf("template execution: %w", err)
    }

    return nil
}

Custom Functions

// customFunctions returns template helper functions
func (m *Manager) customFunctions() template.FuncMap {
    return template.FuncMap{
        // String manipulation
        "lower": strings.ToLower,
        "upper": strings.ToUpper,
        "title": strings.Title,

        // Date formatting
        "formatDate": func(date string) string {
            if date == "" {
                return "Present"
            }
            t, err := time.Parse("2006-01", date)
            if err != nil {
                return date
            }
            return t.Format("Jan 2006")
        },

        // Collections
        "join": strings.Join,

        // Conditionals
        "eq": func(a, b interface{}) bool {
            return a == b
        },

        // HTML
        "safe": func(s string) template.HTML {
            return template.HTML(s)
        },
    }
}

Template Method Pattern Example

Data Processing Pipeline

// DataProcessor defines template method
type DataProcessor struct {
    data []byte
}

// Process is the template method (algorithm skeleton)
func (p *DataProcessor) Process() error {
    // Step 1: Validate
    if err := p.Validate(); err != nil {
        return fmt.Errorf("validation: %w", err)
    }

    // Step 2: Parse
    parsed, err := p.Parse()
    if err != nil {
        return fmt.Errorf("parsing: %w", err)
    }

    // Step 3: Transform
    transformed, err := p.Transform(parsed)
    if err != nil {
        return fmt.Errorf("transform: %w", err)
    }

    // Step 4: Save
    if err := p.Save(transformed); err != nil {
        return fmt.Errorf("save: %w", err)
    }

    return nil
}

// Customizable steps
func (p *DataProcessor) Validate() error {
    if len(p.data) == 0 {
        return errors.New("empty data")
    }
    return nil
}

func (p *DataProcessor) Parse() (interface{}, error) {
    var result interface{}
    err := json.Unmarshal(p.data, &result)
    return result, err
}

func (p *DataProcessor) Transform(data interface{}) (interface{}, error) {
    // Transform logic
    return data, nil
}

func (p *DataProcessor) Save(data interface{}) error {
    // Save logic
    return nil
}

Interface-Based Template Method

// Define steps as interface
type Validator interface {
    Validate() error
}

type Parser interface {
    Parse([]byte) (interface{}, error)
}

type Transformer interface {
    Transform(interface{}) (interface{}, error)
}

// Pipeline uses interfaces for customization
type Pipeline struct {
    validator   Validator
    parser      Parser
    transformer Transformer
}

func NewPipeline(v Validator, p Parser, t Transformer) *Pipeline {
    return &Pipeline{
        validator:   v,
        parser:      p,
        transformer: t,
    }
}

// Process is template method
func (p *Pipeline) Process(data []byte) (interface{}, error) {
    // Fixed algorithm, customizable steps
    if err := p.validator.Validate(); err != nil {
        return nil, err
    }

    parsed, err := p.parser.Parse(data)
    if err != nil {
        return nil, err
    }

    result, err := p.transformer.Transform(parsed)
    if err != nil {
        return nil, err
    }

    return result, nil
}

Template Pattern in Handler Processing

Request Processing Template

// Handler follows template method for all requests
func (h *CVHandler) processRequest(
    w http.ResponseWriter,
    r *http.Request,
    templateName string,
) error {
    // Step 1: Get preferences (same for all)
    prefs := middleware.GetPreferences(r)

    // Step 2: Validate language (same for all)
    lang := r.URL.Query().Get("lang")
    if lang == "" {
        lang = prefs.CVLanguage
    }
    if err := validateLanguage(lang); err != nil {
        return err
    }

    // Step 3: Prepare data (same algorithm, different data)
    data, err := h.prepareTemplateData(lang)
    if err != nil {
        return err
    }

    // Step 4: Render template (different template name)
    if err := h.tmpl.Render(w, templateName, data); err != nil {
        return TemplateError(err)
    }

    return nil
}

// Handlers use the template
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    if err := h.processRequest(w, r, "index.html"); err != nil {
        h.HandleError(w, r, err)
    }
}

func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
    if err := h.processRequest(w, r, "partials/cv_content.html"); err != nil {
        h.HandleError(w, r, err)
    }
}

Function-Based Template Pattern

Using Higher-Order Functions

// Template function accepts customization functions
func ProcessWithTemplate(
    validate func() error,
    transform func() (interface{}, error),
    save func(interface{}) error,
) error {
    // Template algorithm
    if err := validate(); err != nil {
        return err
    }

    data, err := transform()
    if err != nil {
        return err
    }

    return save(data)
}

// Usage with closures
err := ProcessWithTemplate(
    func() error {
        // Custom validation
        return validateInput(input)
    },
    func() (interface{}, error) {
        // Custom transformation
        return transformData(input)
    },
    func(data interface{}) error {
        // Custom save
        return db.Save(data)
    },
)

Template Caching Pattern

Cache Management

// Template cache with thread-safe access
type TemplateCache struct {
    templates map[string]*template.Template
    mu        sync.RWMutex
}

// Get retrieves from cache (or loads if missing)
func (c *TemplateCache) Get(name string) (*template.Template, error) {
    // Try read lock first
    c.mu.RLock()
    tmpl, ok := c.templates[name]
    c.mu.RUnlock()

    if ok {
        return tmpl, nil
    }

    // Not found, load with write lock
    c.mu.Lock()
    defer c.mu.Unlock()

    // Double-check after acquiring write lock
    if tmpl, ok := c.templates[name]; ok {
        return tmpl, nil
    }

    // Load template
    tmpl, err := template.ParseFiles(name)
    if err != nil {
        return nil, err
    }

    // Cache it
    c.templates[name] = tmpl
    return tmpl, nil
}

Benefits

  1. Consistency: Algorithm is consistent across all uses
  2. Customization: Steps can be customized without changing algorithm
  3. Code Reuse: Common algorithm logic is reused
  4. Maintainability: Changes to algorithm are centralized
  5. Testability: Steps can be tested independently

Real-World Use Cases

1. HTTP Request Processing

// All requests follow same template
func (h *Handler) handleRequest(
    w http.ResponseWriter,
    r *http.Request,
    process func() (interface{}, error),
) {
    // 1. Authentication
    user := authenticate(r)

    // 2. Authorization
    if !authorize(user, r) {
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }

    // 3. Process (customizable)
    result, err := process()
    if err != nil {
        h.handleError(w, err)
        return
    }

    // 4. Respond
    json.NewEncoder(w).Encode(result)
}

2. Data Migration

// Migration template
type Migration interface {
    Up() error
    Down() error
}

type MigrationRunner struct {
    migrations []Migration
}

func (r *MigrationRunner) Run() error {
    for _, m := range r.migrations {
        // Template: Begin → Execute → Commit/Rollback
        tx := db.Begin()

        if err := m.Up(); err != nil {
            tx.Rollback()
            return err
        }

        tx.Commit()
    }
    return nil
}

3. Test Setup/Teardown

// Test template
type TestCase struct {
    Name  string
    Setup func() error
    Run   func() error
    Teardown func() error
}

func RunTestCase(tc *TestCase) error {
    // Template algorithm
    if err := tc.Setup(); err != nil {
        return fmt.Errorf("setup: %w", err)
    }

    err := tc.Run()

    // Always teardown, even on error
    if teardownErr := tc.Teardown(); teardownErr != nil {
        return fmt.Errorf("teardown: %w", teardownErr)
    }

    return err
}

Best Practices

DO

// Define clear algorithm skeleton
func (p *Processor) Process() error {
    if err := p.step1(); err != nil {
        return err
    }
    if err := p.step2(); err != nil {
        return err
    }
    return p.step3()
}

// Use interfaces for flexibility
type Step interface {
    Execute() error
}

// Document the template algorithm
// Process executes the full processing pipeline:
// 1. Validate input
// 2. Transform data
// 3. Save result
func (p *Processor) Process() error {
    // ...
}

// Make steps testable independently
func TestValidate(t *testing.T) {
    p := &Processor{}
    err := p.Validate()
    // test validation logic
}

DON'T

// DON'T make algorithm too rigid
// Allow customization where appropriate

// DON'T mix concerns
// Keep template method focused on algorithm,
// not implementation details

// DON'T over-complicate
// If algorithm is simple, don't force template pattern

Testing Template Methods

func TestTemplateManager_Render(t *testing.T) {
    // Test template algorithm
    cfg := &config.TemplateConfig{
        Dir:         "testdata/templates",
        PartialsDir: "testdata/partials",
        HotReload:   false,
    }

    manager, err := NewManager(cfg)
    if err != nil {
        t.Fatal(err)
    }

    // Test each step
    t.Run("LoadTemplates", func(t *testing.T) {
        if len(manager.templates) == 0 {
            t.Error("expected templates to be loaded")
        }
    })

    t.Run("Render", func(t *testing.T) {
        var buf bytes.Buffer
        data := map[string]string{"name": "Test"}

        err := manager.Render(&buf, "test.html", data)
        if err != nil {
            t.Errorf("render failed: %v", err)
        }

        if buf.Len() == 0 {
            t.Error("expected rendered output")
        }
    })
}
  • Strategy Pattern: Both allow algorithm customization
  • Factory Pattern: Often used with template for object creation
  • Handler Pattern: Uses template method for request processing

Further Reading