skip to content

What does html/template escape automatically that text/template leaves untouched?

level: juniorimportance: must knowfreq 60%

answer

  1. same API, different guarantee
  2. one writes the bytes verbatim
  3. the other reads the markup around it
  4. the import path is the whole switch
  5. text/template escapes nothing at all

basics

~20 s

html/template escapes every value it substitutes, choosing the escaping from where the value lands in the page: HTML text, an attribute, a script block or a URL. text/template escapes nothing and writes the value as-is.

solid answer

~40 s

The two packages expose the same API — `New`, `Parse`, `Execute`, `Must`, `FuncMap` — so the same template text runs under either. The difference is behaviour at render time. `text/template` writes each value's textual form verbatim, so a comment containing `<script>alert(1)</script>` reaches the page as markup. `html/template` wraps that engine with a contextual escaping pass: it reads the surrounding markup, works out what the output position is at every action, and rewrites the parse tree so each value goes through the matching escaper. The same input renders as `&lt;script&gt;alert(1)&lt;/script&gt;`. For anything served as HTML you import `html/template`; `text/template` is for output that is not HTML — plain-text mail, config files, generated code.

code

go · 8 lines
go
const page = `<p>{{.}}</p>`
hostile := "<script>alert(1)</script>"

_ = texttemplate.Must(texttemplate.New("t").Parse(page)).Execute(os.Stdout, hostile)
// <p><script>alert(1)</script></p>

_ = template.Must(template.New("h").Parse(page)).Execute(os.Stdout, hostile)
// <p>&lt;script&gt;alert(1)&lt;/script&gt;</p>

go deeper

for a junior

Be ready to name the package you would import to render a web page and say why. Knowing that one escapes for you and the other does not is the whole first answer.

for a middle

Explain that html/template mirrors text/template's API and adds an escaping pass over the parse tree, so the switch is an import change that alters runtime behaviour rather than template syntax.

for a senior

Show how you keep the wrong package out of a service: HTML rendering behind one path, a review or lint rule on text/template imports in the view layer, and a render test that feeds a hostile string through.

for a principal

Own the standard across services: one sanctioned rendering path for HTML, and a stated position on where text/template is legitimate — code generation, plain-text mail, config — so teams are not deciding it case by case.

## Two packages, one API Go ships two template engines with deliberately identical surfaces. `text/template` is the engine: it parses template text into a tree of actions (`{{.Field}}`, `{{range}}`, `{{if}}`, pipelines) and executes that tree against a data value, writing to an `io.Writer`. `html/template` re-exports the same names — `Template`, `New`, `Must`, `Parse`, `ParseFiles`, `Execute`, `ExecuteTemplate`, `FuncMap` — and internally delegates parsing and execution to `text/template`. Switching from one to the other is normally a one-line change to the import path; the template text itself does not change. What `html/template` adds is an escaping pass. Between parsing and execution it walks the parse tree, parses the literal markup around each action, and rewrites the pipeline of every action so that the substituted value is passed through an escaping function appropriate to the position it will occupy in the output. ## What `text/template` does with a value Nothing. It formats the value the way `fmt` would and writes those bytes. If the value is `<script>alert(1)</script>`, those exact bytes land in the response. `text/template` does provide predefined functions named `html`, `js` and `urlquery` that you can apply by hand in a pipeline, and its own documentation notes that `html` is unavailable in `html/template` with a few exceptions. Applying them by hand is not the same thing as contextual escaping: you have to remember every action, and you have to pick the right one for the position yourself. ## What `html/template` does with the same value It escapes according to context. In body text, `<`, `>` and `&` become entities and quotes become numeric references — `"` renders as `&#34;`. Inside a quoted attribute value the same entity escaping applies. Inside a `<script>` block the value is emitted as a JavaScript literal, so a string arrives already quoted. Inside a URL attribute such as `href` the value is percent-encoded and passed through a scheme filter, so a `javascript:` URL is replaced by the marker `#ZgotmplZ` rather than being emitted. You do not choose any of this; the package chooses it from the markup you wrote around the action. A direct consequence: you do not add your own quoting. Writing `var name = "{{.Name}}"` and writing `var name = {{.Name}}` are both handled, but they are handled differently, and hand-escaping on top of the package is at best redundant — `html/template` can even reject a pipeline that manually applies the predefined `html` or `urlquery` escaper, because it has already escaped for you. ## Why the choice of package is a real decision Because the API is identical, importing the wrong package compiles, passes every test that only checks for expected text, and produces working-looking pages. The failure is silent and only visible with hostile input. That is why the usual rule in a Go service is blunt: the view layer imports `html/template` and nothing else; anywhere `text/template` appears in code that produces a response body, it is a review finding. `text/template` remains the right tool for output that is not a web page: a generated `.go` file, a Kubernetes-style YAML manifest, a plain-text notification, a report written to stdout by a CLI. There the escaping `html/template` performs would be wrong — it would mangle the output — and there is no HTML parser to give the value a meaningful position. ## The mental model to keep `text/template` answers "put this value here". `html/template` answers "put this value here, in a form that cannot change the structure of the document around it". The second question is only answerable if the engine understands the document, which is exactly why the escaping engine is a separate package with an HTML parser inside it rather than a flag on the first one.

  • If both packages have the same API, what actually changes in your code when you switch to html/template?
    Usually only the import path. The types and function names line up, so `template.Must(template.ParseFiles(...))` and `Execute` keep compiling. What changes is runtime behaviour: values are now escaped per context, some pipelines that hand-applied `html` or `urlquery` may be rejected, and a template whose markup has no well-defined output position now fails when it is executed.
  • When is text/template still the right package?
    Whenever the output is not HTML: generated Go source, config and manifest files, plain-text email bodies, CLI reports, SQL-free text fixtures. There is no document structure for a contextual escaper to protect, and `html/template`'s entity escaping would corrupt the output.
  • Does escaping user input when it is stored remove the need for html/template?
    No. Escaping at storage time bakes one output context into the data, so the same value is wrong everywhere else — an entity-escaped string inside a script block or a URL is both broken and unsafe. Store the value as the user typed it and let the template escape it for the position it actually occupies.

text/template is a mail-merge that pastes whatever you hand it. html/template is a typesetter that knows whether the slot is a headline, a footnote or a hyperlink, and reformats the text to fit that slot.

saying these in an interview costs you the question

  • Believes text/template escapes HTML because it has an html function
  • Says html/template just runs HTMLEscapeString on every value
  • Thinks escaping at input time makes the template package irrelevant
  • Claims switching packages requires rewriting the template text
  • Uses text/template for a response body because it is faster