8.15 text/template and html/template — Tasks¶
Twelve exercises in increasing difficulty. Most expect the standard library only; a few stretch into
embed,io/fs, andnet/http. Solutions deliberately not provided — try, then compare with the corresponding section of the leaf docs.
Task 1 — Hello, html/template¶
Write a program that:
- Defines an inline
html/templatewith the body<p>Hello, {{.Name}}.</p>. - Calls
Executeagainstos.StdoutwithName: "<script>x</script>". - Verifies (in a test, with
bytes.Buffer) that the output contains<script>and not<script>.
Then change the import to text/template and observe the failure. This is the textbook XSS demonstration.
Task 2 — A cat-like template renderer¶
Write a CLI that takes a template file path and a JSON file path, parses the template (text/template), unmarshals the JSON into a map[string]any, and renders the result to stdout.
Requirements:
- Use
os.ReadFilefor both inputs. - Set
missingkey=errorand propagate the error to the user. - Exit code 1 with a useful message on any failure.
Task 3 — Email composer¶
Build an email composer with text/template:
Compose(e Email) (subject, body string, err error) parses e.Subject and e.Body independently, executes both against e.Data, and returns the rendered strings.
Bonus: support a custom dateFormat function.
Task 4 — A simple HTTP page renderer¶
Write a server that serves three pages (/, /about, /contact) from html/template files in templates/pages/. Use a single base.html layout with {{block "title"}} and {{block "content"}}.
Requirements:
- Render-to-buffer-first.
Content-Type: text/html; charset=utf-8andX-Content-Type-Options: nosniff.- Parse all templates at startup with
template.Must. - Set
Option("missingkey=error").
Task 5 — embed.FS migration¶
Take the result of Task 4 and switch from disk-loading to embed.FS:
Use template.ParseFS(templatesFS, ...). Verify that the binary runs after go build from any directory (no relative path dependency).
Task 6 — Hot reload toggle¶
Extend Task 5 with a --dev flag. When set, templates are loaded from os.DirFS("./templates") and re-parsed on every request. When not set, they are loaded from embed.FS and parsed once at startup. Same renderer code path; differ only in the fs.FS and the cache flag.
Bonus: in dev mode, watch the directory with fsnotify and reparse only when files change.
Task 7 — A FuncMap with an audit trail¶
Build a FuncMap that includes:
upper(string) string→strings.ToUpperhumanize(int64) string→ "1.5 KB", "3.2 MB", etc.fmtTime(time.Time) string→"2006-01-02 15:04"default(string, string) string→ second arg if first is""safeHTML(string) template.HTML→ marks input as trusted
The last one is the audit point. Place a // SAFETY: ... comment explaining why callers can trust it (or, if they can't, when not to use it).
Add a unit test that renders a template using each function and asserts the output.
Task 8 — Determinism test¶
Given a map[string]string data input with keys "b": "B", "a": "A", write a template:
Assert the output is a=A;b=B; deterministically across 100 executions in a row, on the same template, with the same data. This verifies the lexical-key-order guarantee.
Task 9 — <no value> detector¶
Write a helper RenderStrict(t *template.Template, data any) (string, error) that:
- Renders into a buffer.
- After execution, scans the output for the literal substring
<no value>and returns an error if found.
This is a belt-and-suspenders check on top of missingkey=error — it catches sources of <no value> other than missing map keys.
Task 10 — Layout with sub-templates¶
Build the following template set:
base.html— layout with{{block "title"}},{{block "head"}},{{block "content"}},{{block "footer"}}.partials/nav.html— nav bar (defines templatenav).partials/footer.html— defines templatefooter-content.pages/home.html— definestitle,content(uses{{template "nav" .}}).pages/about.html— definestitle,content, and overridesheadwith a custom meta tag.
Render both pages from one Go program. Assert the about page's output contains the custom meta and the home page's does not.
Task 11 — Streaming NDJSON with text/template¶
Write a CSV-like exporter that, given iter.Seq[Row], streams each row through a text/template:
into an io.Writer. Use bufio.Writer between the template and the underlying writer; remember to Flush.
Test: pass 100k rows and verify peak memory stays bounded (use runtime.MemStats before and after).
Task 12 — XSS fuzzer¶
Write a fuzz test for a html/template of your choosing:
func FuzzRender(f *testing.F) {
f.Add("benign")
f.Fuzz(func(t *testing.T, s string) {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, struct{ X string }{s}); err != nil {
return
}
out := strings.ToLower(buf.String())
for _, danger := range []string{"<script", "javascript:", " onerror=", " onclick="} {
if strings.Contains(out, danger) {
t.Fatalf("escape failed for %q: %q", s, out)
}
}
})
}
Run for a minute (-fuzz=. -fuzztime=1m). Confirm no failures, then deliberately replace the string field with template.HTML and verify the fuzz catches the bug.
Stretch tasks¶
S1 — A markdown-to-HTML pipeline with sanitization¶
Wire goldmark (or any Markdown library) to produce HTML from user-supplied Markdown, then run it through bluemonday (or another sanitizer) before wrapping as template.HTML. Add a test that an attempted XSS via Markdown ([click](javascript:x)) is neutralized.
S2 — Custom delimiters for a JS-templating context¶
Use Delims("[[", "]]") to render a text/template whose output contains literal {{ and }} (e.g., a Vue.js template you're generating with Go). Verify the output is byte-equal to the expected.
S3 — Inline render vs buffered render benchmark¶
Benchmark two render strategies: streaming directly to io.Discard vs render to bytes.Buffer then WriteTo. Use a moderately complex html/template with a 50-row range. Report allocs/op for both.
S4 — Plug a render trace into OpenTelemetry¶
Wrap your Render method to start an OTel span with the template name, recording duration as an attribute and recording an exception on error. Verify the spans appear in your collector or in tracetest.
S5 — Build a precompile tool¶
Write a go run ./cmd/precompile that walks a templates directory, parses every file, and reports parse errors. Wire it into CI as a guard before build, so a typo in a template fails the pipeline at compile-time, not deploy-time.
Self-check¶
After working through these:
- You know which package to import for which output.
- You can build a layout with
block/definefrom scratch. - You can wire
embed.FSandos.DirFSto the same renderer. - You can write a goldenfile + fuzz test for the auto-escaper.
- You can audit a codebase for unsafe
template.HTML(...)calls. - You know the cost of
text/templatefor HTML output (one XSS ticket, minimum).
If any of these are still fuzzy, re-read the corresponding section in middle.md or senior.md.