Template Engine

Exercise: Template Engine

Difficulty - Intermediate

Learning Objectives

  • Master text/template and html/template packages
  • Learn template syntax and functions
  • Practice template composition and inheritance
  • Implement custom template functions
  • Build safe HTML rendering

Problem Statement

Create a templates package for rendering dynamic content with templates.

Implementation

  1package templates
  2
  3import (
  4	"bytes"
  5	"html/template"
  6	"strings"
  7	"time"
  8)
  9
 10// TemplateEngine manages templates
 11type TemplateEngine struct {
 12	templates *template.Template
 13}
 14
 15// New creates a new template engine
 16func New() *TemplateEngine {
 17	tmpl := template.New("").Funcs(template.FuncMap{
 18		"upper":      strings.ToUpper,
 19		"lower":      strings.ToLower,
 20		"title":      strings.Title,
 21		"formatDate": formatDate,
 22		"add":        add,
 23		"multiply":   multiply,
 24		"truncate":   truncate,
 25	})
 26
 27	return &TemplateEngine{
 28		templates: tmpl,
 29	}
 30}
 31
 32// Custom template functions
 33func formatDate(t time.Time, format string) string {
 34	if format == "" {
 35		format = "2006-01-02"
 36	}
 37	return t.Format(format)
 38}
 39
 40func add(a, b int) int {
 41	return a + b
 42}
 43
 44func multiply(a, b int) int {
 45	return a * b
 46}
 47
 48func truncate(s string, length int) string {
 49	if len(s) <= length {
 50		return s
 51	}
 52	return s[:length] + "..."
 53}
 54
 55// ParseString parses a template string
 56func ParseString(name, content string) error {
 57	_, err := te.templates.New(name).Parse(content)
 58	return err
 59}
 60
 61// Render renders a template with data
 62func Render(name string, data interface{}) {
 63	var buf bytes.Buffer
 64	err := te.templates.ExecuteTemplate(&buf, name, data)
 65	if err != nil {
 66		return "", err
 67	}
 68	return buf.String(), nil
 69}
 70
 71// Example templates
 72const (
 73	emailTemplate = `
 74Hello {{.Name | upper}},
 75
 76Thank you for signing up on {{formatDate .SignupDate "January 2, 2006"}}.
 77
 78Your account details:
 79- Email: {{.Email}}
 80- Plan: {{.Plan | title}}
 81- Users: {{.UserCount}}
 82
 83{{if gt .UserCount 10}}
 84You have a large team! Consider upgrading to Enterprise.
 85{{else}}
 86You're on the {{.Plan}} plan.
 87{{end}}
 88
 89Best regards,
 90The Team
 91`
 92
 93	htmlTemplate = `
 94<!DOCTYPE html>
 95<html>
 96<head>
 97	<title>{{.Title}}</title>
 98</head>
 99<body>
100	<h1>{{.Heading}}</h1>
101
102	{{if .Items}}
103	<ul>
104		{{range .Items}}
105		<li>{{.}}</li>
106		{{end}}
107	</ul>
108	{{else}}
109	<p>No items to display.</p>
110	{{end}}
111
112	<footer>
113		Generated on {{formatDate .Timestamp "Mon Jan 2 15:04:05 MST 2006"}}
114	</footer>
115</body>
116</html>
117`
118
119	reportTemplate = `
120{{define "header"}}
121=== {{.Title}} ===
122Date: {{formatDate .Date "2006-01-02"}}
123{{end}}
124
125{{define "body"}}
126Summary:
127{{range .Items}}
128- {{.Name}}: {{.Value}}
129{{end}}
130
131Total: {{.Total}}
132{{end}}
133
134{{define "report"}}
135{{template "header" .}}
136
137{{template "body" .}}
138{{end}}
139`
140)
141
142// Example usage
143type User struct {
144	Name       string
145	Email      string
146	SignupDate time.Time
147	Plan       string
148	UserCount  int
149}
150
151type ReportData struct {
152	Title string
153	Date  time.Time
154	Items []struct {
155		Name  string
156		Value int
157	}
158	Total int
159}
160
161func Example() error {
162	engine := New()
163
164	// Parse email template
165	if err := engine.ParseString("email", emailTemplate); err != nil {
166		return err
167	}
168
169	// Render email
170	user := User{
171		Name:       "alice",
172		Email:      "alice@example.com",
173		SignupDate: time.Now(),
174		Plan:       "professional",
175		UserCount:  15,
176	}
177
178	output, err := engine.Render("email", user)
179	if err != nil {
180		return err
181	}
182	println(output)
183
184	// Parse HTML template
185	if err := engine.ParseString("html", htmlTemplate); err != nil {
186		return err
187	}
188
189	// Render HTML
190	htmlData := map[string]interface{}{
191		"Title":     "My Page",
192		"Heading":   "Welcome",
193		"Items":     []string{"Item 1", "Item 2", "Item 3"},
194		"Timestamp": time.Now(),
195	}
196
197	htmlOutput, err := engine.Render("html", htmlData)
198	if err != nil {
199		return err
200	}
201	println(htmlOutput)
202
203	// Parse report template with nested templates
204	if err := engine.ParseString("report", reportTemplate); err != nil {
205		return err
206	}
207
208	// Render report
209	reportData := ReportData{
210		Title: "Monthly Report",
211		Date:  time.Now(),
212		Items: []struct {
213			Name  string
214			Value int
215		}{
216			{"Sales", 1000},
217			{"Expenses", 500},
218		},
219		Total: 500,
220	}
221
222	reportOutput, err := engine.Render("report", reportData)
223	if err != nil {
224		return err
225	}
226	println(reportOutput)
227
228	return nil
229}

Solution

Click to see the complete solution

The implementation demonstrates a production-ready template engine using Go's html/template package. Key concepts:

Template Syntax

1. Variables and Data:

1{{.Name}}           <!-- Access field -->
2{{.User.Email}}     <!-- Nested field -->
3{{index .Items 0}}  <!-- Index into slice/array -->

2. Control Structures:

 1<!-- If/Else -->
 2{{if .Condition}}
 3	Content when true
 4{{else}}
 5	Content when false
 6{{end}}
 7
 8<!-- Range -->
 9{{range .Items}}
10	<li>{{.}}</li>
11{{else}}
12	<p>No items</p>
13{{end}}
14
15<!-- With -->
16{{with .User}}
17	<p>{{.Name}}</p>
18{{end}}

3. Comparisons:

1{{if eq .Status "active"}}Active{{end}}
2{{if ne .Count 0}}Count: {{.Count}}{{end}}
3{{if gt .Price 100}}Expensive{{end}}
4{{if and .IsAdmin .IsActive}}Admin{{end}}

4. Pipelines:

1{{.Name | upper}}                    <!-- Function call -->
2{{.Date | formatDate "2006-01-02"}}  <!-- With argument -->
3{{.Text | truncate 50 | upper}}      <!-- Chaining -->

Best Practices

1. Use html/template for Web:

1// Automatically escapes HTML to prevent XSS
2import "html/template"  // Not "text/template"
3
4// Input: <script>alert('xss')</script>
5// Output: &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;

2. Custom Functions:

1funcs := template.FuncMap{
2	"upper": strings.ToUpper,
3	"add": func(a, b int) int { return a + b },
4	"formatDate": func(t time.Time, layout string) string {
5		return t.Format(layout)
6	},
7}
8
9tmpl := template.New("").Funcs(funcs)

3. Template Composition:

 1<!-- Define reusable templates -->
 2{{define "header"}}
 3	<header>{{.Title}}</header>
 4{{end}}
 5
 6{{define "footer"}}
 7	<footer>{{.Year}}</footer>
 8{{end}}
 9
10<!-- Use them -->
11{{template "header" .}}
12<main>Content</main>
13{{template "footer" .}}

4. Error Handling:

1var buf bytes.Buffer
2err := tmpl.ExecuteTemplate(&buf, "page", data)
3if err != nil {
4	// Handle template execution error
5	return err
6}

5. Caching Templates:

1var templates = template.Must(template.ParseGlob("templates/*.html"))
2
3func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
4	err := templates.ExecuteTemplate(w, name, data)
5	if err != nil {
6		http.Error(w, err.Error(), http.StatusInternalServerError)
7	}
8}

Security

Auto-Escaping:

1<!-- Safe: html/template auto-escapes -->
2<div>{{.UserInput}}</div>
3
4<!-- Unsafe: bypasses escaping -->
5<div>{{.TrustedHTML | safeHTML}}</div>

Context-Aware Escaping:

1<!-- Different escaping in different contexts -->
2<a href="{{.URL}}">Link</a>          <!-- URL escaping -->
3<script>var x = "{{.JSValue}}"</script>  <!-- JS escaping -->
4<style>.class { color: {{.Color}} }</style>  <!-- CSS escaping -->

Performance

Pre-parse Templates:

1// Parse once at startup, not on every request
2var templates = template.Must(template.ParseFiles(
3	"templates/base.html",
4	"templates/home.html",
5	"templates/about.html",
6))

Use Buffering:

1// Buffer output to catch errors before sending to client
2var buf bytes.Buffer
3if err := tmpl.Execute(&buf, data); err != nil {
4	return err  // Template error, don't send partial response
5}
6w.Write(buf.Bytes())

Key Takeaways

  1. Custom Functions: Extend templates with custom FuncMap
  2. HTML Safety: html/template auto-escapes to prevent XSS
  3. Template Composition: Use {{define}} and {{template}} for reuse
  4. Conditionals: Use {{if}} for conditional rendering
  5. Loops: Use {{range}} to iterate over slices/maps
  6. Security: Context-aware escaping protects against injection
  7. Performance: Pre-parse and cache templates