Day 48 — templates & regexp

Updated

July 30, 2026

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 strings functions when regex is overkill.
  • Avoid catastrophic backtracking patterns on untrusted input (pathological regex DoS).
  • MustCompile at 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/day48

Lab 2 — Text report

  1. Parse template once (sync.Once or package init / Must).
  2. Render sample inventory.
  3. Table test: empty items still prints count 0.
  4. FuncMap: qtyStatus returns "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 > out

JSON 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.