34. HTML templates
Chapter 33 ended with a problem: printing a visitor's text into HTML
lets the visitor write part of your page. Go's
html/template package solves this. You write the page once,
with gaps in it, and the package fills the gaps and escapes the values
for you.
A first template
A template is text with actions
between {{ and }}. Here is
exercises/34/01-first-template.go:
// Chapter 34 example: a template in a string, filled from a struct.
// Run with: go run exercises/34/01-first-template.go
package main
import (
"fmt"
"html/template"
"net/http"
)
type playerPage struct {
Name string
Level int
}
const playerText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Name}}</title>
</head>
<body>
<h1>{{.Name}}</h1>
<p>Level {{.Level}}</p>
</body>
</html>
`
var playerTemplate = template.Must(template.New("player").Parse(playerText))
func handleHome(w http.ResponseWriter, r *http.Request) {
data := playerPage{Name: "Keiko", Level: 7}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
err := playerTemplate.Execute(w, data)
if err != nil {
fmt.Println("Template error:", err)
}
}
func main() {
http.HandleFunc("GET /{$}", handleHome)
fmt.Println("Server running at http://localhost:8080")
fmt.Println("Press Ctrl+C to stop it.")
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
fmt.Println("Server stopped:", err)
}
}Open http://localhost:8080, and the page shows the
heading Keiko and the line Level 7. The body
of the response contains:
<title>Keiko</title>
<h1>Keiko</h1>
<p>Level 7</p>
Three things are new.
{{.Name}} means "put the
Name field here". The dot is the value you passed in.
template.Must(template.New("player").Parse(playerText))
reads from the inside out. template.New makes an empty
template with a name, which appears in its error messages.
Parse reads your text, and returns the template and an
error. template.Must takes both, and crashes the program if
the error is not nil. Crashing sounds rude, but this runs
once, when the program starts, on text you wrote. A mistake there can
never be caused by a visitor, and it is better found at once. So a
variable outside main can hold the result. For example, a
missing } on the <h1> line, as in
{{.Name}, stops the program straight away:
panic: template: player:8: bad character U+007D '}'
playerTemplate.Execute(w, data) fills
in the gaps and writes the result to w.
Fields must be exported
The template package is a different package from yours, so Chapter
25's rule applies: it can see only names that start with a capital
letter. With a field called name and {{.name}}
in the template, the page stops partway, and the terminal says:
Template error: template: player:5:9: executing "player" at <.name>: name is an unexported field of struct type main.playerPage
If you change the field to name and leave the template
with {{.Name}}, the message is different, but the cause is
the same:
Template error: template: player:5:9: executing "player" at <.Name>: can't evaluate field Name in type main.playerPage
Go cannot catch these while compiling, because a template is only a string until the program runs. Go's compiler checks your code, and not your templates.
When Execute fails
That error has a catch. Execute writes as it goes along,
so by the time it fails, part of the page has gone out with status 200.
It is too late to change the status.
exercises/34/02-execute-error.go shows this, with a
template that asks for a field that does not exist:
curl.exe -i http://localhost:8080/direct
HTTP/1.1 200 OK
<h1>Keiko</h1>
<p>
The visitor gets half a page, marked as a success. The fix is to build the page in memory first, and send it only if that worked:
func handleBuffered(w http.ResponseWriter, r *http.Request) {
data := pageData{Name: "Keiko"}
var page bytes.Buffer
err := brokenTemplate.Execute(&page, data)
if err != nil {
fmt.Println("Template error:", err)
http.Error(w, "The page could not be built.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
page.WriteTo(w)
}bytes.Buffer from the bytes package is a
place in memory to write to. Execute accepts it for the
same reason that it accepts w. Its first parameter is an
interface, io.Writer, and any type with the right
Write method satisfies it, as in Chapter 29.
Write has a pointer receiver on the buffer, so you pass
&page. Forgetting the & stops the
compiler:
cannot use page (variable of type bytes.Buffer) as io.Writer value in argument to brokenTemplate.Execute: bytes.Buffer does not implement io.Writer (method Write has pointer receiver)
page.WriteTo(w) sends what the buffer holds. Now the
result is honest:
curl.exe -i http://localhost:8080/buffered
HTTP/1.1 500 Internal Server Error
The page could not be built.
The terminal gets the details, and the visitor gets a short message:
Template error: template: broken:2:5: executing "broken" at <.Missing>: can't evaluate field Missing in type main.pageData
Details of your program are for you, not for visitors. From here on, every handler builds its page in a buffer.
range and if
Templates have loops and conditions. Here is the template in
exercises/34/03-range-and-if.go:
<ul>
{{range .Items}}
<li>{{.Name}}{{if .Rare}} (rare){{end}}</li>
{{else}}
<li>Nothing here yet.</li>
{{end}}
</ul>And the data it gets:
type item struct {
Name string
Rare bool
}
type listPage struct {
Title string
Items []item
}{{range .Items}}repeats everything up to its{{end}}, once per item. Inside it, the dot changes to the current item, so{{.Name}}is that item's name.{{else}}in a range runs when the slice is empty. That is the place for a message likeNothing here yet.{{if .Rare}}shows its content when the value is true. For strings, numbers and slices, the empty value counts as false, so{{if .Error}}asks whether there is an error message to show.
Both routes in this program use the same template. Blank lines are left out here:
curl.exe http://localhost:8080/
<h1>My bag</h1>
<ul>
<li>sword</li>
<li>moon stone (rare)</li>
<li>potion</li>
</ul>
curl.exe http://localhost:8080/empty
<h1>An empty bag</h1>
<ul>
<li>Nothing here yet.</li>
</ul>
In the real output, the template's line endings around
{{range}} leave blank lines in the HTML. Browsers ignore
them, so they do no harm.
Open exercises/34/03-range-and-if.go. Add a field
Weight int to item, give each item a weight,
and show it in the list. Remember to use a capital letter.
Automatic escaping
Here is the echo from Chapter 33, made safe. This is
exercises/34/04-safe-echo.go:
// Chapter 34 example: the echo from Chapter 33, made safe with html/template.
// Run with: go run exercises/34/04-safe-echo.go
package main
import (
"bytes"
"fmt"
"html/template"
"net/http"
)
type echoPage struct {
Text string
}
const echoText = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Echo</title>
</head>
<body>
<p>You said: {{.Text}}</p>
<form action="/echo" method="get">
<p><input type="text" name="text" value="{{.Text}}"></p>
<p><button type="submit">Say it again</button></p>
</form>
</body>
</html>
`
var echoTemplate = template.Must(template.New("echo").Parse(echoText))
func handleEcho(w http.ResponseWriter, r *http.Request) {
data := echoPage{Text: r.URL.Query().Get("text")}
var page bytes.Buffer
err := echoTemplate.Execute(&page, data)
if err != nil {
fmt.Println("Template error:", err)
http.Error(w, "The page could not be built.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
page.WriteTo(w)
}
func main() {
http.HandleFunc("GET /echo", handleEcho)
fmt.Println("Server running at http://localhost:8080")
fmt.Println("Press Ctrl+C to stop it.")
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
fmt.Println("Server stopped:", err)
}
}Send the same attack as before, with the same kind of command:
curl.exe -G --data-urlencode "text=<script>alert('hello')</script>" http://localhost:8080/echo
The two lines of the body that hold the text are:
<p>You said: <script>alert('hello')</script></p>
<p><input type="text" name="text" value="<script>alert('hello')</script>"></p>
Your handler does nothing special. html/template
replaced each special character with a code that a browser
shows and does not run. The visitor
sees the text <script>alert('hello')</script>
on the page, as ordinary text, and no box pops up. Open
http://localhost:8080/echo?text=<script>alert('hello')</script>
to compare it with Chapter 33.
The package also knows where in the page each gap is. Look at the
second line above: the value is inside an attribute, between
" and ", where a " would end it.
An attack that tries to break out of the attribute gets its quotes
escaped too:
<p><input type="text" name="text" value="" onmouseover="alert(1)"></p>
Why not text/template
Go has a second package with nearly the same functions,
text/template. It does no escaping, because it is for text
that is not HTML. exercises/34/06-text-template-danger.go
puts the same input through both. It has no server, and it uses the
import aliases from Chapter 25 because both packages are called
template:
// Chapter 34 example: the same input through text/template and html/template.
// This program has no server. It prints both results and stops.
// Run with: go run exercises/34/06-text-template-danger.go
package main
import (
"fmt"
htmltemplate "html/template"
"os"
texttemplate "text/template"
)
const pageText = "<p>{{.}}</p>\n"
const input = `<script>alert("hello")</script>`
func main() {
wrong := texttemplate.Must(texttemplate.New("wrong").Parse(pageText))
right := htmltemplate.Must(htmltemplate.New("right").Parse(pageText))
fmt.Println("text/template:")
err := wrong.Execute(os.Stdout, input)
if err != nil {
fmt.Println("Error:", err)
}
fmt.Println("html/template:")
err = right.Execute(os.Stdout, input)
if err != nil {
fmt.Println("Error:", err)
}
}Output:
text/template:
<p><script>alert("hello")</script></p>
html/template:
<p><script>alert("hello")</script></p>
The code is otherwise identical, and the wrong import compiles and
looks fine. So check the import line: for a web page, it must be
html/template. An editor's automatic import can
pick the wrong one.
A template in a file
A template of more than a few lines is easier to edit in a file of
its own. The file needs to be found when the program runs, so this
example is a module with its own folder,
exercises/34/05-template-file/:
05-template-file/
go.mod
main.go
templates/
scores.html
templates/scores.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Scores of {{.Player}}</title>
</head>
<body>
<h1>Scores of {{.Player}}</h1>
<ul>
{{range .Scores}}
<li>{{.}}</li>
{{end}}
</ul>
</body>
</html>Inside {{range .Scores}}, each item is a plain int, so
{{.}} means the value itself.
main.go:
// Chapter 34 example: a template in its own file.
//
// Run from this folder, because the template path is relative to it:
// cd exercises/34/05-template-file
// go run .
package main
import (
"bytes"
"fmt"
"html/template"
"net/http"
)
type scorePage struct {
Player string
Scores []int
}
var scoreTemplate *template.Template
func handleHome(w http.ResponseWriter, r *http.Request) {
data := scorePage{Player: "Keiko", Scores: []int{40, 25, 70}}
var page bytes.Buffer
err := scoreTemplate.Execute(&page, data)
if err != nil {
fmt.Println("Template error:", err)
http.Error(w, "The page could not be built.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
page.WriteTo(w)
}
func main() {
parsed, err := template.ParseFiles("templates/scores.html")
if err != nil {
fmt.Println("Could not read the template:", err)
fmt.Println("Run this program from its own folder, and check the template.")
return
}
scoreTemplate = parsed
http.HandleFunc("GET /{$}", handleHome)
fmt.Println("Server running at http://localhost:8080")
fmt.Println("Press Ctrl+C to stop it.")
err = http.ListenAndServe("localhost:8080", nil)
if err != nil {
fmt.Println("Server stopped:", err)
}
}template.ParseFiles reads and parses the file, and
returns the template and an error. This time the error can happen for
reasons outside the code, so main checks it, prints a clear
message, and stops. It does not use template.Must, because
a crash is a poor way of telling a person that they are in the wrong
folder.
The path is relative to the folder that the program runs from. So run it from its own folder:
cd exercises/34/05-template-file
go run .
To go back up to go-work afterwards:
cd ../../..
Try running it from go-work by naming the file. It
compiles, because this program has only one .go file, and
then it cannot find the template:
go run exercises/34/05-template-file/main.go
Could not read the template: open templates/scores.html: no such file or directory
Run this program from its own folder, and check the template.
On Windows, the end of the first line is worded differently: it says that the system cannot find the path specified.
The page in this file, like every page in this book, uses nothing from other servers. There are no outside stylesheets, scripts or fonts. Each one would mean a request to another computer every time someone opens your page.
Go into exercises/34/05-template-file. Add a heading to
templates/scores.html, and restart the server. The file is
read once, at the start, so the change does not appear until you
restart.
Homework
Homework starters are in exercises/34/homework/. Each
one runs as it is and shows a placeholder. The challenge is a module, so
cd into its folder and use go run ..
Problem 1 — A profile page
Open exercises/34/homework/01-profile.go. Define a
struct with the fields Name, City and
Level. In handleHome, fill one with your own
values, and pass it to the template. Finish the template so that it
shows all three.
Problem 2 — A task list
Open exercises/34/homework/02-task-list.go. The tasks
are in a package-level slice, and nothing changes them. Finish the
template: show one <li> per task, add
(done) after the name of a finished task, and show
Nothing to do. when the slice is empty. The slice itself is
passed to Execute, so {{range .}} loops over
it.
Problem 3 — A safe greeting
Open exercises/34/homework/03-safe-greeting.go.
GET /greet?name=Keiko shows Hello, Keiko! on
an HTML page. Use a template, so that a name such as
<b>Keiko</b> shows up as text and not in bold.
Check it with curl.exe, and look for <
in the reply. A missing or empty name replies with status 400.
Challenge — Players from a template file
Folder: exercises/34/homework/04-players/.
Finish templates/players.html so that it lists the
players, and says so when there are none. Then finish
handleHome in main.go. GET /
shows every player, and GET /?min=50 shows only players
with a score of 50 or more. A min that is not a whole
number replies with status 400. The form on the page shows the
min that was asked for.
Stuck or finished? Open the homework solutions page.