📝 Analogy
A template is a fill-in-the-blanks form: you write the fixed text once, mark the blanks with {{…}} actions, then hand the engine a data value to fill them. The text/template engine fills any text; its twin html/template fills HTML and checks the ink — escaping anything that could turn data into executable markup. For the web you always want the careful twin.
text/template basics
You parse a template string into a *template.Template, then execute it against a data value, writing the result to any io.Writer. Actions in {{…}} pull from the current data context, written as the dot (.). {{.Field}} reads an exported struct field, a map key, or a zero-argument method:
package main
import (
"os"
"text/template"
)
type Invoice struct {
Customer string
Total float64
}
func main() {
// Must panics on a parse error — good for templates known at startup.
t := template.Must(template.New("inv").Parse(
"Dear {{.Customer}}, your total is {{printf \"$%.2f\" .Total}}.\n",
))
t.Execute(os.Stdout, Invoice{Customer: "Ada", Total: 123.456})
// Dear Ada, your total is $123.46.
}
Control flow: if, range, with
Templates have just enough logic to shape output: {{if}}/{{else}} for conditionals, {{range}} to iterate (re-binding the dot to each element), and {{with}} to narrow the dot to a sub-value. There are no arbitrary expressions — that’s deliberate; logic belongs in Go, presentation in the template:
package main
import (
"os"
"text/template"
)
type Cart struct {
User string
Items []string
}
func main() {
const page = "{{.User}}'s cart:\n" +
"{{range .Items}} - {{.}}\n{{else}} (empty)\n{{end}}" +
"{{if .Items}}Total items: {{len .Items}}\n{{end}}"
t := template.Must(template.New("cart").Parse(page))
t.Execute(os.Stdout, Cart{User: "Bob", Items: []string{"pen", "ink"}})
t.Execute(os.Stdout, Cart{User: "Cleo"}) // empty cart hits {{else}}
}
Functions and pipelines
A template can call functions. Built-ins include len, printf, index, and comparisons (eq, lt, …). You add your own via a template.FuncMap — attached with .Funcs(...) before parsing, because the parser must recognize the names. The pipeline operator | feeds one result into the next, just like a shell:
package main
import (
"os"
"strings"
"text/template"
)
func main() {
funcs := template.FuncMap{
"upper": strings.ToUpper,
"repeat": strings.Repeat,
}
t := template.Must(
template.New("f").Funcs(funcs).Parse(
"{{.Name | upper}} {{repeat \"!\" 3}}\n",
),
)
t.Execute(os.Stdout, map[string]string{"Name": "go"})
// GO !!!
}
html/template: auto-escaping for safety
Switching the import to html/template keeps the identical API but adds the feature that matters for the web: context-aware auto-escaping. The engine tracks whether a value is landing in HTML text, an attribute, a URL, or a <script> block, and escapes it the right way for that spot. This is what stops user input like <script>steal()</script> from becoming executable — the cross-site scripting (XSS) defense you get for free:
package main
import (
"html/template"
"os"
)
func main() {
t := template.Must(template.New("p").Parse(
"<p>Hello, {{.}}</p>\n",
))
// Hostile input is neutralized automatically.
t.Execute(os.Stdout, "<script>steal()</script>")
// <p>Hello, <script>steal()</script></p>
t.Execute(os.Stdout, "Ada & Bob")
// <p>Hello, Ada & Bob</p>
}
The same data through text/template would emit the raw <script> tag verbatim — a live XSS hole. That difference is the whole reason both packages exist.
graph LR
D["data value"] --> T{"which package?"}
T -->|text/template| RAW["bytes emitted verbatim<br/>(reports, configs, code-gen)"]
T -->|html/template| ESC["context-aware escaping<br/>(HTML · attr · JS · URL)"]
ESC --> SAFE["safe HTML — XSS neutralized"]text/template vs html/template
text/template | html/template | |
|---|---|---|
| API | identical | identical (drop-in) |
| Escaping | none — verbatim | automatic, context-aware |
| Use for | emails, configs, code generation, CLI output | any HTML served to a browser |
| XSS safety | your responsibility | handled by the engine |
| Underlying | text/template | wraps text/template + escaper |
Under the hood: parse once, execute many
Parsing compiles the template text into an internal tree; executing walks that tree against your data. Parsing is the expensive part, so do it once — at package init or server startup — and execute per request. A *template.Template is safe for concurrent Execute calls once parsed, so one parsed template serves all your goroutines. Real apps usually template.ParseGlob("templates/*.html") to load a whole directory into one template set, then ExecuteTemplate(w, "page.html", data) to render a named one — letting templates {{define}} and {{template}} include each other for layouts.
⚠️ Use html/template for the web, and parse before serving
Two rules. (1) For anything a browser renders, use html/template — text/template does zero escaping and turns user data into an injection vector. Don’t defeat the escaper by marking untrusted data template.HTML. (2) Parse templates once at startup with template.Must, not on every request — re-parsing per request is slow and a missing-file error would surface mid-request instead of at boot. Also: only exported struct fields are visible to templates (same reflection rule as JSON).
See also
- encoding/json — the other way to turn Go data into a wire format; both rely on exported fields.
- http-server — render
html/templateoutput straight to thehttp.ResponseWriter. - strings & bytes —
strings.Builderis a handyExecutetarget when you want a string. - fmt & io —
Executewrites to anyio.Writer.
Next: working with dates, durations and timers — time.
Related topics
Turning Go values into JSON and back — Marshal/Unmarshal, struct tags and omitempty, decoding into structs vs maps, streaming Encoder/Decoder, custom Marshaler/Unmarshaler, and json.RawMessage for deferred decoding.
essentialsstrings & bytesThe text toolkit — searching and transforming with the strings package, O(n) assembly via strings.Builder, the parallel bytes package and bytes.Buffer, strconv for number⇄string conversion, and the unicode/utf8 view of multibyte text.
Check your understanding
Score: 0 / 51. What is the single most important reason to use html/template instead of text/template for web pages?
html/template understands where in the document a value lands and escapes it appropriately, neutralizing injected <script> and similar. text/template emits bytes verbatim — using it for HTML is an XSS hole.
2. In a template, what does {{.Name}} mean?
The dot (.) is the current data context. {{.Name}} accesses the Name exported field of a struct, the "Name" key of a map, or a zero-arg method Name. Inside {{range}} or {{with}}, the dot is rebound to the element.
3. Why is template.Must(template.New(...).Parse(...)) a common idiom?
Templates defined as constants in source either parse or they don't — there's no runtime recovery. template.Must wraps the (\*Template, error) pair and panics on error, so a typo surfaces at program start, not on first request.
4. What does {{range .Items}} … {{end}} do?
range iterates a slice, array, map, or channel. Inside the block, . becomes the current element (use {{.}} or {{.Field}}). An optional {{else}} runs when the collection is empty.
5. How do you add a custom function (e.g. uppercase) usable inside a template?
template.FuncMap maps names to Go functions; attach it with .Funcs(...) BEFORE Parse (the parser must know the names). Then invoke as {{upper .Name}} or via a pipeline {{.Name | upper}}.
Comments
Sign in with GitHub to join the discussion.