d95c62bad4
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.
14 KiB
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
- Consistency: Algorithm is consistent across all uses
- Customization: Steps can be customized without changing algorithm
- Code Reuse: Common algorithm logic is reused
- Maintainability: Changes to algorithm are centralized
- 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")
}
})
}
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