Day 48 — templates & regexp
Day 48 — templates & regexp
Stage V · ~3h
Goal: Use text/template and html/template correctly, compile regexp once, and complete one real text task (report rendering + field extraction) with tests.
Why this day exists
Not every interface is JSON. You will:
- Emit emails, CLI reports, config snippets →
text/template
- Emit HTML →
html/template(auto-escaping)
- Validate/extract tokens →
regexp
Mixing up HTML vs text templates is an XSS footgun. Compiling regexps in hot loops is a performance footgun.
Theory 1 — text/template
const reportTmpl = `Items: {{len .Items}}
{{range .Items}}- {{.Name}} ({{.ID}})
{{end}}`
t, err := template.New("report").Parse(reportTmpl)
if err != nil {
return err
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return err
}Actions (core)
| Action | Meaning |
|---|---|
{{.Field}} |
Print field |
{{if .OK}}...{{end}} |
Conditional |
{{range .Items}}...{{end}} |
Iterate |
{{with .User}}...{{end}} |
Set dot |
{{define "x"}}...{{end}} |
Named template |
{{template "x" .}} |
Invoke |
FuncMap
t, err := template.New("r").Funcs(template.FuncMap{
"upper": strings.ToUpper,
"ms": func(d time.Duration) int64 { return d.Milliseconds() },
}).Parse(`{{upper .Name}} took {{ms .Dur}}ms`)Functions must be registered before Parse.
Theory 2 — html/template auto-escape
import htmltemplate "html/template"
t, err := htmltemplate.New("page").Parse(`<h1>{{.Title}}</h1><p>{{.Body}}</p>`)If Body is <script>alert(1)</script>, it is escaped for HTML context. Context-aware escaping also handles attributes, JS, CSS URLs—still do not disable escaping casually.
// Dangerous — only for trusted, already-sanitized HTML:
htmltemplate.HTML("<b>trusted</b>")Rule: user content → normal pipeline; never cast to HTML without a sanitizer policy.
Theory 3 — regexp
var tokenRE = regexp.MustCompile(`^[a-z][a-z0-9_-]{2,31}$`)
if !tokenRE.MatchString(s) {
return fmt.Errorf("invalid token")
}| API | Use |
|---|---|
Compile / MustCompile |
Once at init |
MatchString |
Boolean |
FindStringSubmatch |
Capture groups |
ReplaceAllString |
Rewrite |
Split |
Divide |
Submatches
re := regexp.MustCompile(`(?P<user>[^@]+)@(?P<host>[^@]+)`)
m := re.FindStringSubmatch("ada@example.com")
// m[0] full; m[1] user; m[2] host
// names: re.SubexpNames()Performance & safety
- Prefer simple
stringsfunctions when regex is overkill.
- Avoid catastrophic backtracking patterns on untrusted input (pathological regex DoS).
MustCompileat package init panics on bad patterns—good for static patterns.
Theory 4 — When not to use either
| Problem | Prefer |
|---|---|
| Structured API output | encoding/json |
| Simple prefix/suffix | strings.Cut* |
| Binary protocols | custom codecs / protobuf later |
| SQL | query builders / sqlc—not templates for values |
Never assemble SQL with templates/string concat for user input.
Worked example — inventory report + ID extract
package report
import (
"bytes"
"fmt"
"regexp"
"text/template"
"time"
)
var idRE = regexp.MustCompile(`\bid=([A-Za-z0-9_-]+)\b`)
type Item struct {
ID string
Name string
Qty int
}
type Data struct {
GeneratedAt time.Time
Items []Item
}
const tmpl = `Inventory report {{.GeneratedAt.Format "2006-01-02 15:04:05 MST"}}
Count: {{len .Items}}
{{range .Items}}- {{.Name}} [{{.ID}}] qty={{.Qty}}
{{end}}`
func Render(data Data) (string, error) {
t, err := template.New("inv").Parse(tmpl)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
func ExtractIDs(text string) []string {
matches := idRE.FindAllStringSubmatch(text, -1)
out := make([]string, 0, len(matches))
for _, m := range matches {
if len(m) > 1 {
out = append(out, m[1])
}
}
return out
}
func ValidateID(id string) error {
if !regexp.MustCompile(`^[A-Za-z0-9_-]+$`).MatchString(id) {
return fmt.Errorf("bad id")
}
return nil
}Improve ValidateID by lifting the regexp to package level (lab requirement).
Lab 1 — Setup
mkdir -p ~/lab/90daysofx/01-go/day48
cd ~/lab/90daysofx/01-go/day48
go mod init example.com/day48Lab 2 — Text report
- Parse template once (
sync.Onceor packageinit/Must).
- Render sample inventory.
- Table test: empty items still prints count 0.
- FuncMap:
qtyStatusreturns"low"if qty < 3 else"ok".
Lab 3 — HTML page (escape proof)
// title/body from "user"
// assert output does not contain raw <script>Render with html/template; input Body=<script>alert(1)</script>; assert escaped entities in output.
Lab 4 — Regexp extract
Given log lines:
level=info id=abc-01 msg=created
level=error id=xyz_2 msg=failed
Extract all ids with compiled regexp; test expected slice.
Lab 5 — CLI
go run . -in items.json -format text|html > outJSON in → template out. Reuse Day 41 skills lightly.
Common gotchas
| Gotcha | Fix |
|---|---|
text/template for HTML |
Use html/template |
HTML cast on user data |
Don’t |
| FuncMap after Parse | Register before Parse |
| Compile regexp per request | Package-level MustCompile |
| Greedy catastrophic patterns | Simplify; bound input |
| Template SQL | Parameterized queries only |
Forgetting Execute error |
Always check |
Checkpoint
- Text report template with range/if
- FuncMap used
- HTML auto-escape proven by test
- Regexp compiled once; extract + validate
- Can explain text vs html package choice
- CLI or library API documented
Commit
git add .
git commit -m "day48: templates and regexp report pipeline"Write three personal gotchas before continuing.
Tomorrow
Day 49 — httptest: table-driven handler tests for your API—status, headers, body, and middleware behavior without a real port.