---
title: Templates
source: 'https://academia.sh/en/courses/go-applications/templates'
course: 'The Standard Library and Network Applications'
language: en
updated: '2026-09-14T10:59:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows by running it that the text produced by two different Go types run through the same template is byte-for-byte identical'
    - "Tests that a field name appears in the output only if it was written into the template's own text"
    - 'Compares how text and HTML templates diverge on the same data with respect to escaping'
    - 'Shows that an expression asking for a nonexistent field stops the template at run time, but the output produced before the stop remains'
---

# Templates

The same template text, run with two different Go types (an integer, a decimal number), produces byte-for-byte identical output; the source's type information is lost entirely at the output. A field name appears in the output only if it was written into the template's own text — it does not come from the type system. Text and HTML templates turn the same data into different byte sequences with respect to escaping. The limiting measure: a template looks up field names at run time, a nonexistent field only becomes known at execution, not at compile time, and the error is born in the middle of the output.

The previous lesson's last measurement showed what a struct tag is for: a struct tag was a
schema that carried a field name to the other side of the boundary, and without that schema
there was no rebuilding either. This lesson asks the same question in reverse. JSON
serialization turned a struct into bytes and brought it back — there was a schema between
the outbound trip and the return, and the schema rebuilt the type of five of the four fields.
Templates never promise this return at all: what a template produces is only text, and that
text is assumed never to be read again with any schema. The course's question stays the
same — what happens to a value's type information as it crosses a boundary? — but this time
the other side of the boundary is not a program, it is a screen or a file.

Go's standard library carries two template packages: `text/template` and `html/template`.
Both use the same template language — `{{.Field}}` reads a field, `{{if}}` and `{{range}}`
provide branching and iteration — and both take a Go value and write text to an
`io.Writer`. The only difference between them is the escaping behavior measured shortly.
Both are called by their package name in the code blocks; in the prose they are referred to
only as "the text template" and "the HTML template."

The `Execute` method's second parameter is an `io.Writer` — this lesson uses a buffer
(`bytes.Buffer`), but the same call could just as well write to a file, standard output, or
a network connection; the System and Data Access topic's first lesson will separately
measure that the same interface is shared between a file and a buffer. The template package
itself does not know, and does not need to know, what the destination is — it only produces
and writes a sequence of bytes. This lesson measures exactly what does and does not stand
inside these produced bytes: as the source data crosses the boundary, does the type system
stay on Go's own side, or does it carry through to the text the template produces?

The course's shared measurement split the thirteen boundary crossings into three classes:
type is preserved in one, rebuilt in five, lost in seven (the full distribution built in
Lesson 03). Template output is one of these seven lost crossings — but the **shape** of the
loss differs from JSON's loss, because JSON is a round-trip format (converted to bytes, then
readable back), while a template never promises a return at all; it is not even assumed that
the text it produces will ever be parsed again.

## A Template's Only Output Type: Text

**ND19.** What a template's `Execute` call produces, no matter what the source data's type
is, is always a byte sequence — and once that byte sequence is turned into a string, it
carries none of the source's type information. The most direct way to show this is to run
the same template text with two different Go types and compare the outputs.

```go
// templates.go — measures the type of a template's output, field traces, the escaping difference, and run-time lookup
package main

import (
	"bytes"
	"fmt"
	"html/template"
	"strings"
	textTemplate "text/template"
)

type Product struct {
	Name     string
	Quantity int
	Comment  string
}

type ProductInt struct{ Quantity int }
type ProductFloat struct{ Quantity float64 }

func main() {
	product := Product{Name: "bolt", Quantity: 12, Comment: "good <b>but</b> expensive & slow"}

	fixedTemplate := textTemplate.Must(textTemplate.New("t").Parse("{{.Quantity}}"))
	var bufFromInt, bufFromFloat bytes.Buffer
	fixedTemplate.Execute(&bufFromInt, ProductInt{Quantity: 12})
	fixedTemplate.Execute(&bufFromFloat, ProductFloat{Quantity: 12})
	fmt.Println("-- same template, two different source types --")
	fmt.Println("output from the int:", bufFromInt.String())
	fmt.Println("output from the float:", bufFromFloat.String())
	fmt.Println("are the two outputs byte-for-byte the same:", bufFromInt.String() == bufFromFloat.String())

	narrow := textTemplate.Must(textTemplate.New("narrow").Parse("{{.Name}} x{{.Quantity}}"))
	var bufNarrow bytes.Buffer
	narrow.Execute(&bufNarrow, product)
	fmt.Println()
	fmt.Println("-- a field name appears in the output only if the template's own text wrote it --")
	fmt.Println("narrow template output:", bufNarrow.String())
	fmt.Println("does \"Name\" appear in the output:", strings.Contains(bufNarrow.String(), "Name"))
	fmt.Println("does \"Quantity\" appear in the output:", strings.Contains(bufNarrow.String(), "Quantity"))

	wide := textTemplate.Must(textTemplate.New("wide").Parse("Name: {{.Name}}, Quantity: {{.Quantity}}"))
	var bufWide bytes.Buffer
	wide.Execute(&bufWide, product)
	fmt.Println("wide template output:", bufWide.String())
	fmt.Println("does \"Name\" appear in the output:", strings.Contains(bufWide.String(), "Name"))

	textTmpl := textTemplate.Must(textTemplate.New("m").Parse("{{.Comment}}"))
	var bufText bytes.Buffer
	textTmpl.Execute(&bufText, product)
	htmlTmpl := template.Must(template.New("h").Parse("{{.Comment}}"))
	var bufHtml bytes.Buffer
	htmlTmpl.Execute(&bufHtml, product)
	fmt.Println()
	fmt.Println("-- the same data produces different bytes from a text and an html template --")
	fmt.Printf("text template: %s\n", bufText.String())
	fmt.Printf("html template: %s\n", bufHtml.String())
	fmt.Println("are the two outputs the same:", bufText.String() == bufHtml.String())

	broken := textTemplate.Must(textTemplate.New("broken").Parse("before: {{.Name}}, after: {{.MissingField}}"))
	var bufBroken bytes.Buffer
	err := broken.Execute(&bufBroken, product)
	fmt.Println()
	fmt.Println("-- a nonexistent field errors at run time, the part written before it stays written --")
	fmt.Println("did an error occur:", err != nil)
	fmt.Printf("part that reached the output before the error: %q\n", bufBroken.String())
}
```

```
-- same template, two different source types --
output from the int: 12
output from the float: 12
are the two outputs byte-for-byte the same: true

-- a field name appears in the output only if the template's own text wrote it --
narrow template output: bolt x12
does "Name" appear in the output: false
does "Quantity" appear in the output: false
wide template output: Name: bolt, Quantity: 12
does "Name" appear in the output: true

-- the same data produces different bytes from a text and an html template --
text template: good <b>but</b> expensive & slow
html template: good &lt;b&gt;but&lt;/b&gt; expensive &amp; slow
are the two outputs the same: false

-- a nonexistent field errors at run time, the part written before it stays written --
did an error occur: true
part that reached the output before the error: "before: bolt, after: "
```

**ND20.** `ProductInt.Quantity` is an `int`, `ProductFloat.Quantity` is a `float64` — when
both are run through the same `{{.Quantity}}` template, the output is the string `12` in
both cases, byte-for-byte equal. While the template was running it knew what type the source
came from (`Execute` takes an interface value and reaches its field through reflection), but
no trace of this information remains in the text it produces. A program reading the output
cannot work backward from the string `12` to answer the question "was this an integer or a
decimal?"

This lands on the same surface as the previous lesson's untyped-container observation, but
by a different route. When a JSON value was decoded into an untyped container, a number
always became `float64` — there was a loss, but the loss had **a specific direction**: every
number fell into the same single type, and that fall was `encoding/json`'s own rule.
Template output does not even have a rule like this, because there is no target type at all
— `int` and `float64` both fall to the same `string`, but this fall has no structure like
"converging on a single target type"; both became ordinary text either way, and the text itself
belongs to no type. JSON's loss was a **converter's** decision; the template's loss is the
result of there being **no target type at all** to convert to.

**ND21.** In the `bolt x12` text the narrow template (`{{.Name}} x{{.Quantity}}`) produces,
neither the word `"Name"` nor `"Quantity"` appears — field names are lost just as much as
type information, because what the template writes is the field's **value**, not the
field's **name**. `"Name"` does appear in the wide template's output (`Name: {{.Name}},
Quantity: {{.Quantity}}`), but not because Go automatically carries the struct field's name
along — the person who wrote the template typed the string `"Name: "` into the template by
hand, as plain text. A field name appearing in the output is not a guarantee coming from the
type system, it is a decision made by whoever wrote the template text.

A template's output being pure text carries a flexibility as much as it carries a
limitation: the destination does not have to be an HTML document. `text/template` can also
be used to produce a configuration file, an email body, a log line, even a source file
written in another language — whatever is produced, it makes no difference to the template
package's eyes, because the package only translates one piece of text into another,
substituting field values in place. The course's next lesson picks up exactly this last use,
source code generation, from precisely this point.

`ProductInt` and `ProductFloat` were deliberately defined as two parallel, single-field
types: the only difference between them is the type of the `Quantity` field, nothing else
changes. This makes clear that what the measurement shows is only "type information is
lost," not something caused by another structural difference (field count, field order,
field name) — the two types respond to the same template action (`{{.Quantity}}`) under the
same name, and because type is the only variable between them, the equality in the output
can be attributed directly to that single variable.

## The Escaping Difference Between Text and HTML Templates

**ND22.** The same `Product` value, with the same `{{.Comment}}` template text, produces two
different byte sequences when given to two different packages. `text/template` writes the
`<b>` and `&` characters in the `Comment` field exactly as they are, without touching them at
all. `html/template` turns the same characters into the strings `&lt;`, `&gt;`, and `&amp;`
— this transformation is **escaping** (the term established in the M14/K01 Foundations of
the Web and HTML course). `html/template` does this automatically because it assumes its
output will be placed inside an HTML document, and knows that the characters `<`, `>`, `&`
have a special meaning in HTML; `text/template` makes no such assumption, because it does
not know where its output is going.

The M14/K01 course built HTML generation and escaping behavior on the browser side, in the
context of processing a document arriving from a server; that is not repeated here, the only
difference is that these two packages stand inside the same standard library, as a
production step on the server side. The choice between the two packages is not a matter of
style: if the output is going into an HTML page, `html/template` must be used, because a
text template that skips escaping produces an output that writes a string like `<script>` in
the data exactly as it is, if one appears.

`html/template`'s escaping works in more detail than it appears in this measurement: the
package detects **where** in the template a field is used (inside a tag body, an attribute
value, a link address) during parsing, and applies the escaping rule appropriate to each
context — a quotation mark inside an attribute value and a `<` character inside a tag body
are escaped by different rules. This lesson only measures the simplest context (a tag body);
context awareness itself is a separate subject and is not deepened here. The important
result is this: `text/template` has no idea in what context its output will be used and
changes no characters at all, while `html/template` assumes its output will be placed into
an HTML document and behaves accordingly. When unescaped user input is placed as it is
inside an HTML document, the browser may read it not as plain text but as **structure** —
escaping itself is a step that changes not the appearance of the value passing through, but
how the other side interprets it.

## A Field Name Is Looked Up at Run Time

**ND23.** The expression `{{.MissingField}}` asks for a field that does not exist on the
`Product` type. This template compiles — because the template text is a string written in a
separate language Go's compiler never sees; the compiler knows which fields the `Product`
type has but never reads the expression `{{.MissingField}}` inside a string. The field
lookup happens through reflection **at run time**, when `Execute` is called, and when the
field cannot be found at that moment, `Execute` returns an error. A question similar to the
course's "the counterpart of what was never written" (the axis established in M09/K01, not
reused here) also holds here: a layer the compiler never looks at produces its own error on
its own schedule.

A template's own error moments are not singular either. The `template.Must` call wraps the
error `Parse` returns and panics — this never triggered throughout this lesson, because
every template's text was syntactically valid (every `{{` always closed with a `}}`, every
action was a recognized action). A `Parse` error, for an unclosed `{{` fragment for example,
comes out right at the start of the program, while the template is being built. An `Execute`
error, on the other hand, comes out only when the requested field is missing, while the
syntax is entirely valid. Both happen at run time (Go's compiler sees neither), but one
surfaces the moment the template is built, the other only when it is actually run with real
data — and this lesson only measured the second one, because no example that would trigger
the first was used.

**ND24.** The `broken` template carries the text `"before: {{.Name}}, after:
{{.MissingField}}"` and contains two separate actions: first `{{.Name}}` evaluates
successfully and the text `"before: bolt, after: "` is written to the `bufBroken` buffer;
then `{{.MissingField}}` fails and `Execute` returns an error. But the buffer is not rolled
back — the part written before the error stays where it is, because `Execute` writes text
**progressively**, it does not accumulate a result and write it all at once at the end. This
explains where the string `"before: bolt, after: "` seen in the measurement's last line
comes from: the error is born in the middle of the output, while the second field is being
looked up, and everything produced up to that point has already reached the `io.Writer`.

**ND25.** This adds a new angle to the course's distinction between "rejected" and "left
exposed." The compiler never sees the field name error — the source compiles under every
condition — and the error surfaces neither at compile time nor while the template is being
parsed (in the `Parse` call); only when `Execute` runs, and the output written at that
moment of execution becomes permanent. The only way to test a template before putting it
into production is to **run** it with real data; a tool that statically checks the
correctness of field names falls outside this pair.

The practical consequence of this is that a rarely triggered branch inside a template (a
line that only passes through under a `{{if}}` condition with specific data) can go months
without ever running at all. The Go Command Family lesson of the Go Fundamentals course
showed that the vet tool catches a pattern the compiler misses — a mismatch between a format
string and an argument's type. There is no second checker like this for template field
names; the template language itself lives outside Go's type system, in a separate language,
and runs in a layer neither the compiler nor the vet tool can reach. This is why a template's
correctness is measured not by the assurance compilation gives, but by the assurance of
having run that template with real data at least once — and this lesson did exactly that, by
running it.

## Summary

- What a template produces is always text; no matter what the source's type is (`int`,
  `float64`), the same template produces the same string, and there is no way to work
  backward from the output to the source's type.
- Field names do not automatically appear in the output either — if a name appears in the
  output, it is because the person who wrote the template typed that name into the template
  as plain text.
- `text/template` and `html/template` share the same template language, but `html/template`
  runs its output through HTML escaping; the same data produces two different byte sequences
  in the two packages.
- A field name in a template is looked up through reflection not at compile time but while
  `Execute` runs; a nonexistent field halts not the template's compilation but its
  execution.
- The error is born in the middle of the output: the part `Execute` wrote before returning
  the error stays permanently in the `io.Writer`, it is not rolled back.

## Next Step

This lesson measured that a template's output is pure text carrying no type — and that no
schema had been set up on the other side to read this text. The difference between this
lesson and the course's previous lesson (JSON serialization) stands exactly here: one is a
trip out that returns with a schema, the other is an exit that never returns at all. But the
two lessons share a common point: in both, the schema, where it existed, stood on only
**one side** of the boundary — the target type on the receiver in JSON, nobody at all in the
template. The next lesson joins these two ends: when two separate sides share the same
schema, what happens to the type information crossing the boundary? Contract-based service
communication answers this question by building the schema itself in a form that generates
both sides' source — and there it builds the single line, among the course's thirteen
boundary crossings, where type information is preserved.
