html/template

A template is HTML with holes in it: parse it once when the program starts, then execute it against data on every request.

Syntaxtemplate.Must(template.New(name).Parse(src)).Execute(w, data)

A template is text with holes in it. You write the HTML once, mark the places where data goes, and then fill those places with values. Go ships two packages for this. text/template fills holes in any text. html/template does the same job but knows the result is HTML, so it escapes the values it inserts. For anything a browser will read, use html/template.

Parse, then execute

The work splits in two. Parsing reads the template text and returns a *template.Template. Executing runs that value against your data and writes the result to an io.Writer, which is the interface the response writer already satisfies. Parsing is the slow half and only needs to happen once, so do it at startup, not inside a handler. A parsed template is safe to execute from many goroutines at the same time.

Templates normally live in files. template.ParseFiles(names...) reads them from disk, and template.ParseFS(fsys, patterns...), added in Go 1.16, reads them from a filesystem value instead.

Inside the template, {{.}} is the data you passed to Execute. The dot is its name. {{.Title}} reads a struct field called Title.

Shorthand and longhand

template.Must is shorthand for checking the error yourself. These are the same thing:

// longhand
t, err := template.New("page").Parse(src)
if err != nil {
	log.Fatal(err)
}

// shorthand, identical behaviour
t := template.Must(template.New("page").Parse(src))

Must panics on an error, which is right at startup: a template that does not parse is a bug you want to see before the program serves anything. Never use Must on something that can fail at request time.

Example

Example · go
package main

import (
	"html/template"
	"log"
	"os"
)

// Only exported (capitalised) fields are visible to a template.
type page struct {
	Title string
	User  string
}

const pageHTML = `<h1>{{.Title}}</h1>
<p>Hello, {{.User}}.</p>
`

// Parsed once, when the program starts. Must panics on a broken template,
// which is a bug you want to see immediately.
var tmpl = template.Must(template.New("page").Parse(pageHTML))

func main() {
	// Execute fills the holes and writes the result to an io.Writer.
	if err := tmpl.Execute(os.Stdout, page{Title: "Dashboard", User: "ada"}); err != nil {
		log.Fatal(err)
	}
}

// Output:
// <h1>Dashboard</h1>
// <p>Hello, ada.</p>

When to use it

  • Render a server-side HTML page from a struct instead of returning JSON.
  • Parse every page template at startup so a syntax error stops the program, not a request.
  • Build a plain-text email body with text/template and the same syntax.
  • Produce an HTML error page when a form submission fails validation.

More examples

Templates in files, parsed at startup

The shape every server-rendered Go app ends up with. Put a templates directory next to the program with a home.html in it, run it, and curl http://localhost:8080/ to see the page. If a file has a syntax error the program refuses to start, which is the point of parsing at startup.

Example · go
package main

import (
	"html/template"
	"log"
	"net/http"
)

// ParseGlob reads every file matching the pattern into one set. Each file's
// base name becomes a template name inside that set.
var pages = template.Must(template.ParseGlob("templates/*.html"))

type home struct {
	Title string
}

func handleHome(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	// ExecuteTemplate picks one template out of the set by name.
	if err := pages.ExecuteTemplate(w, "home.html", home{Title: "Home"}); err != nil {
		log.Print(err)
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", handleHome)
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Render into a buffer, then write

Execute can fail partway through, for example on a field that does not exist. If you executed straight into the response writer, the status line and half a page are already sent and you can no longer return a 500. Buffer first whenever the output goes to a client.

Example · go
package main

import (
	"bytes"
	"html/template"
	"log"
	"os"
)

const rowHTML = "<tr><td>{{.Name}}</td><td>{{.Role}}</td></tr>\n"

type user struct {
	Name string
	Role string
}

func main() {
	t := template.Must(template.New("row").Parse(rowHTML))

	// Execute writes as it goes. Rendering into a buffer first means a failure
	// halfway through never reaches the client.
	var buf bytes.Buffer
	if err := t.Execute(&buf, user{Name: "ada", Role: "admin"}); err != nil {
		log.Fatal(err)
	}
	if _, err := os.Stdout.Write(buf.Bytes()); err != nil {
		log.Fatal(err)
	}
}

// Output:
// <tr><td>ada</td><td>admin</td></tr>

Do not parse inside the handler

Both handlers produce the same HTML, so the difference never shows up in a test. Parsing per request costs disk reads and CPU on every hit, and it moves template errors from startup into production traffic. The one time per-request parsing is defensible is a development mode where you want edits picked up without a restart.

Example · go
package main

import (
	"html/template"
	"log"
	"net/http"
)

const srcHTML = `<h1>{{.}}</h1>
`

// Parsed once. Every request reuses this value, which is safe: a parsed
// template may be executed from several goroutines at the same time.
var page = template.Must(template.New("page").Parse(srcHTML))

func good(w http.ResponseWriter, r *http.Request) {
	if err := page.Execute(w, "hello"); err != nil {
		log.Print(err)
	}
}

func bad(w http.ResponseWriter, r *http.Request) {
	// Re-reads and re-parses the file on every request. Right output, wasted
	// work, and a typo now breaks responses instead of startup.
	t, err := template.ParseFiles("templates/page.html")
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	if err := t.Execute(w, "hello"); err != nil {
		log.Print(err)
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /good", good)
	mux.HandleFunc("GET /bad", bad)
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Discussion

  • Be the first to comment on this lesson.