Actions, Ranges and Conditions
Actions are the {{...}} parts of a template: field reads, range loops, if and with, and the pipeline that joins them.
{{range $i, $v := .Items}}{{$v.Name}}{{end}}Everything between {{ and }} is an action. Text outside the braces is copied to the output unchanged. There are only a handful of actions, and that is deliberate: a template chooses what to show, it does not compute.
Dot and fields
Dot (.) is the current value. When execution starts it is whatever you passed to Execute. {{.Name}} reads a field, {{.User.Email}} a field of a field, and a method that takes no arguments is called the same way. {{$x := .Name}} stores a value in a variable, a name beginning with $. $ on its own always holds the value Execute was given.
range, if, with
{{range .Items}} ... {{end}} runs its body once per element with dot set to that element. {{range $i, $it := .Items}} also binds the index and the element. A {{else}} inside a range runs when there are no elements at all. {{if .Admin}} ... {{else}} ... {{end}} picks a branch, and {{else if}} chains. A value is treated as false when it is zero, an empty string, an empty slice or map, or a nil pointer. {{with .Note}} ... {{end}} is if plus a rebinding of dot: inside it, {{.}} is the note. {{break}} and {{continue}}, both added in Go 1.18, stop a range or skip to its next iteration.
Shorthand and longhand
A pipe passes a value as the last argument of the next function. These two produce identical output:
{{printf "%.2f" .Rate}}
{{.Rate | printf "%.2f"}}
Comparison uses named functions, not symbols: eq, ne, lt, le, gt, ge. Write {{if gt .Qty 1}}; {{if .Qty > 1}} is a parse error.
Example
package main
import (
"html/template"
"log"
"os"
)
type item struct {
Name string
Qty int
}
type order struct {
Items []item
Note string
}
// range sets dot to each element. gt is a function, not an operator.
// with rebinds dot to .Note, so {{.}} inside it is the note.
const orderHTML = `<ul>
{{range .Items}}<li>{{.Name}}{{if gt .Qty 1}} x{{.Qty}}{{end}}</li>
{{end}}</ul>
{{with .Note}}<p>{{.}}</p>
{{end}}`
func main() {
t := template.Must(template.New("order").Parse(orderHTML))
o := order{
Items: []item{{Name: "Pen", Qty: 1}, {Name: "Pad", Qty: 3}},
Note: "leave at the door",
}
if err := t.Execute(os.Stdout, o); err != nil {
log.Fatal(err)
}
}
// Output:
// <ul>
// <li>Pen</li>
// <li>Pad x3</li>
// </ul>
// <p>leave at the door</p>
When to use it
- Render one table row per record returned from a database query.
- Show a "no results" message when a search returns an empty slice.
- Hide an admin link unless the signed-in user is an administrator.
- Print a price to two decimal places with printf instead of formatting it in the handler.
More examples
range with else, and a separator between items
Two jobs that otherwise push people into writing helper functions: joining values with a separator, and handling the empty case. The index variable plus {{if $i}} covers the first, range's own {{else}} covers the second, and neither needs anything from the handler.
package main
import (
"html/template"
"log"
"os"
)
// {{if $i}} is false only for index 0, so the comma appears between items
// and not before the first one. The {{else}} runs when there are none.
const tagsHTML = `{{range $i, $t := .}}{{if $i}}, {{end}}{{$t}}{{else}}no tags{{end}}
`
func main() {
t := template.Must(template.New("tags").Parse(tagsHTML))
for _, tags := range [][]string{{"go", "http", "sql"}, {}} {
if err := t.Execute(os.Stdout, tags); err != nil {
log.Fatal(err)
}
}
}
// Output:
// go, http, sql
// no tags
Reaching the outer data with $
The commonest template bug: a field that renders empty inside a range because dot has moved. Writing {{.Site}} in that loop looks for a Site field on a string and fails at execute time. $ is the escape hatch, and it is also what you need inside {{with}} blocks.
package main
import (
"html/template"
"log"
"os"
)
type page struct {
Site string
Posts []string
}
// Inside the loop, dot is one post. $ still holds what Execute was given,
// so $.Site reaches the field that lives outside the loop.
const postsHTML = `{{range .Posts}}<a href="/p/{{.}}">{{$.Site}}: {{.}}</a>
{{end}}`
func main() {
t := template.Must(template.New("posts").Parse(postsHTML))
p := page{Site: "soundscode", Posts: []string{"go-maps", "go-slices"}}
if err := t.Execute(os.Stdout, p); err != nil {
log.Fatal(err)
}
}
// Output:
// <a href="/p/go-maps">soundscode: go-maps</a>
// <a href="/p/go-slices">soundscode: go-slices</a>
Ranging over a map, in sorted key order
Worth knowing because it differs from the language. A Go for-range over a map visits keys in an unspecified order, but the template packages document that a map whose keys are a basic type with a defined order is visited in sorted key order. So this output is stable, and you do not need to sort the keys in the handler just to get a predictable page.
package main
import (
"html/template"
"log"
"os"
)
// range binds two variables for a map: the key and the value.
const countsHTML = `{{range $lang, $n := .}}{{$lang}}={{$n}}
{{end}}`
func main() {
t := template.Must(template.New("counts").Parse(countsHTML))
counts := map[string]int{"go": 3, "python": 1, "sql": 2}
if err := t.Execute(os.Stdout, counts); err != nil {
log.Fatal(err)
}
}
// Output:
// go=3
// python=1
// sql=2
Discussion