# 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go // 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 ```go 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") } }) } ``` ## Related Patterns - **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 - [Template Method Pattern](https://refactoring.guru/design-patterns/template-method) - [Go Templates](https://pkg.go.dev/text/template) - [html/template Package](https://pkg.go.dev/html/template)