Actions, Ranges and Conditions

Actions are the {{...}} parts of a template: field reads, range loops, if and with, and the pipeline that joins them.

Syntax{{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

Example · go
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.

Example · go
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.

Example · go
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.

Example · go
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

  • Be the first to comment on this lesson.