Crash course on Go templates
A quick talk on everything you need to know
Link to slides ➡
Background Info
- My name is Bok Woon, I am a developer @ Teckwah
- Teckwah is a packaging, printing and logistics company (mainly warehousing)
- Not really Go related, I use Java at work
- The contents for this talk came after I tried writing my own blog from scratch
- Had to learn about html/template in-depth
Content Overview
Part 0 - Introduction to Go templates
Part 1 - Template composition
Part 2 - Syntax and variables
Part 3 - Functions, methods and pipes
Part 0: Introduction to Go templates
What are templates?
- Most commonly used to output HTML (i.e. web pages).
- There are other uses, but I'll focus mainly on HTML.
example output:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
A Hello World example
hello.html
{{ template "header" }}
hello world! my name is {{ .name }}.
{{ template "footer" }}
common.html
{{ define "header" }}
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{{ end }}
{{ define "footer" }}
<p>
Copyright 2021. All rights Reserved.
</p>
{{ end }}
A Hello World example
main.go
t, _ := template.ParseFiles(
"hello.html",
"common.html",
)
t.Execute(os.Stdout, map[string]string{
"name": "bokwoon",
})
output
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
hello world! my name is bokwoon.
<p>
Copyright 2021. All rights Reserved.
</p>
Go's templating system is not like the others
- You may have experience with some other templating engines e.g. django templates, jinja2, laravel blade, etc
- Their template references are static: you include some other template by referencing its file path relative to some directory.
e.g. {% include "partials/header.html" %}
- But in Go, template references are dynamic and you must compose the templates yourself.
e.g. {{ template "header" }}
references a specific template relative to the template dir
which template is "header"�referencing? who knows. depends on what templates you composed together
Part 1: Template Composition
an under-documented aspect of Go templates
btw, if you have any questions just post them in the chat and I'll answer them at the end
In Go, a template is essentially a map[string]*Template
When you compose templates together, you are populating this map[string]*Template data structure
The big idea
In Go, a template is essentially a map[string]*Template
hello.html
<p>Hello World</p>
{{ template "footer" }}
{{ define "footer" }}
<p>
<div>my blog</div>
{{ template "copyright" }}
</p>
{{ end }}
{{ define "copyright" }}
Copyright 2021. All rights reserved.
{{ end }}
map[string]*Template{
"hello.html": &Template{
name: "hello.html",
Tree: /* template body */,
},
"footer": &Template{
name: "footer",
Tree: /* template body */,
},
"copyright": &Template{
name: "copyright",
Tree: /* template body */,
},
}
current template
When you execute {{ template "footer" }}, you are doing a map[string]*Template lookup for "footer"
ParseFiles(
"hello.html",
)
Hello World example, translated to map[string]*Template
hello.html
{{ template "header" }}
hello world! my name is {{ .name }}.
{{ template "footer" }}
common.html
{{ define "header" }}
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{{ end }}
{{ define "footer" }}
<p>
Copyright 2021. All rights Reserved.
</p>
{{ end }}
map[string]*Template{
"hello.html": &Template{
name: "hello.html",
Tree: /* template body */,
},
"common.html": &Template{
name: "common.html",
Tree: /* template body */,
},
"header": &Template{
name: "header",
Tree: /* template body */,
},
"footer": &Template{
name: "footer",
Tree: /* template body */,
},
}
current template
ParseFiles(
"hello.html",
"common.html",
)
A template is essentially a map[string]*Template where one of the templates is marked as the current template
// Get the name of the current template
t.Name() == "hello.html"
// execute the current template
t.Execute(os.Stdout, data)
// execute the "header" template
t.ExecuteTemplate(os.Stdout, "header", data)
// execute the "footer" template
t.ExecuteTemplate(os.Stdout, "footer", data)
// iterate through the templates
for i, t := range t.Templates() { ... }
// get the defined templates (in a string)
fmt.Printf("%s\n", t.DefinedTemplates())
// Set the current template to "header"
t = t.Lookup("header")
// and execute it
t.Execute(os.Stdout, data)
t.Name() == "header"
// If you lookup a nonexistent template,
// you get nil
t.Lookup("jdalsdjlkadaf") == nil
map[string]*Template{
"hello.html": &Template{
name: "hello.html",
Tree: /* template body */,
},
"common.html": &Template{
name: "hello.html",
Tree: /* template body */,
},
"header": &Template{
name: "header",
Tree: /* template body */,
},
"footer": &Template{
name: "footer",
Tree: /* template body */,
},
}
current template
Does Go support nested templates?
- No. Even though a {{ define "header" }} is a nested template definition within a template, all templates are hoisted to the top level i.e. they all exist as equals inside a map[string]*Template
hello.html
{{ template "header" }}
hello world! my name is {{ .name }}.
{{ template "footer" }}
common.html
{{ define "header" }}
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{{ end }}
{{ define "footer" }}
<p>
Copyright 2021. All rights Reserved.
</p>
{{ end }}
main.go
t, _ := template.ParseFiles(
"hello.html",
"common.html",
)
main.go
t, _ := template.New("hello.html").Parse(`
{{ template "header" }}
hello world! my name is {{ .name }}.
{{ template "footer" }}
`)
_, _ = t.New("common.html").Parse("\n")
_, _ = t.New("header").Parse(`
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
`)
_, _ = t.New("footer").Parse(`
<p>
Copyright 2021. All rights Reserved.
</p>
`)
equivalent to
Do Go templates support template inheritance?
Kind of. It's using plain old template composition.
Why is template inheritance used?
- Pages often have a similar HTML structure and the only thing that changes is the main content.
- The problem is, "include" can only ever point to a specific template in the filesystem.
- It can't resolve to a different template depending on the context. It's a static reference.
/blog/posts
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{% include "index.html" %}
<p>
Copyright 2021. All rights Reserved.
</p>
/blog/posts/about-me
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{% include "post.html" %}
<p>
Copyright 2021. All rights Reserved.
</p>
jinja2 example
How does template inheritance solve it?
- Using template inheritance, you define a parent template then let child templates override specific template blocks in it.
- Looking at base.html, what is the "content" block going to render? Who knows. Depends on whether you invoke index.html or post.html or base.html.
base.html
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{% block "content" %}
this is a placeholder
{% endblock %}
<p>
Copyright 2021. All rights Reserved.
</p>
index.html
{% extends "base.html" %}
{% block "content" %}
Here are my posts
{% endblock %}
post.html
{% extends "base.html" %}
{% block "content" %}
this is a post
{% endblock %}
jinja2 example
Go templates do not need template inheritance because template references are already dynamically resolved
- Looking at base.html, what is {{ template "content" }} going to render? Who knows. Depends on whether you ParseFiles("base.html", "index.html") or ParseFiles("base.html", "post.html") or ParseFiles("base.html")
- If you want a template to render a different thing, you just overwrite its definition in the map[string]*Template. Simple.
base.html
<header>
<p><a href="/">Home</a></p>
<p><a href="/about">About Me</a></p>
<p><a href="/contact">Contact</a></p>
</header>
{{ template "content" }}
{{ define "content" }}
this is a placeholder
{{ end }}
<p>
Copyright 2021. All rights Reserved.
</p>
index.html
{{ define "content" }}
Here are my posts
{{ end }}
post.html
{{ define "content" }}
this is a post
{{ end }}
go templates example
In fact, the "block" keyword essentially does the same thing
- {{ block }} is just syntactic sugar for {{ template }} followed by a {{ define }}.
{{ block "content" . }}
this is a placeholder
{{ end }}
{{ template "content" . }}
{{ define "content" }}
this is a placeholder
{{ end }}
equivalent to
part 1 any questions? about template composition
Part 2: Syntax and variables
Syntax keyword overview
1) {{ define ... }}
2) {{ template ... }}
3) {{ block ... }} {{ end }}
4) {{ if <var> }} {{ else if <var> }} {{ else }} {{ end }}
5) {{ with <var> }} {{ end }}
6) {{ range <var> }} {{ else }} {{ end }}
Anything else (and, or, not, len, index etc) is a function
Already covered in part 1
Template Variables
- Every template has access to exactly one variable, denoted by the dot '.'
- This is whatever variable you passed into Execute()/ExecuteTemplate()
e.g. dot is struct{
Params: Params{
Author: Author{
FirstName: "John",
LastName: "Doe",
},
},
}
{{ . }}
{{ .Params.Author.LastName }}
e.g. dot is map[string]interface{}{
"Params": map[string]interface{}{
"Author": map[string]interface{}{
"FirstName": "John",
"LastName": "Doe",
},
},
}
{{ . }}
{{ .Params.Author.LastName }}
alternatively for maps:
{{ index . "Params" "Author" "LastName" }}
Variables must be passed explicitly down to templates
{{ define "footer" }}
{{ .Params.Author.FirstName }}
{{ end }}
// nothing passed down
{{ template "footer" }} // fails
// dot passed down
{{ template "footer" . }} // passes
// subset of dot passed down
{{ define "footer" }}
{{ .FirstName }}
{{ end }}
{{ template "footer" .Params.Author }}
e.g. dot is map[string]interface{}{
"Params": map[string]interface{}{
"Author": map[string]interface{}{
"FirstName": "John",
"LastName": "Doe",
},
},
}
Variables can be assigned (and reassigned)
{{ define "greet" }}
{{ $firstname := .Params.User.FirstName }} // assignment
<p>Hello, my name is {{ $firstname }}.</p>
{{ $firstname = "Joe" }} // reassignment
<p>Actually, my name is {{ $firstname }}.</p>
{{ end }}
- A variable's scope is extends to the {{ end }} keyword of the current block
- i.e. {{ if }}, {{ with }}, {{ range }}, or the current template
4) {{ if <var> }} {{ else if <var> }} {{ else }} {{ end }}
- basic if-statement; you already know how it works
{{ if .User.IsAuthorized }}
<p>Welcome, {{ .User.Name }}!</p>
{{ else }}
<p>You are not allowed to see this</p>
{{ end }}
- Evaluation is truthy/falsy!
- Falsy values are false, 0, nil, empty array/slice/map/string.
- Anything else is truthy.
Don't do ❌
{{ if gt (len .Users) 0 }}
<p>There are {{ len .Users }} users</p>
{{ end }}
Do ✅
{{ if .Users }}
<p>There are {{ len .Users }} users</p>
{{ end }}
The dot '.' and the dollar '$'
- The dot '.' refers to the current template variable
- {{ . }} // print the variable
- {{ .Params.User.FirstName }} // accessing a nested field
- But the dollar '$' also refers to the current template variable
- {{ $ }} // print the variable
- {{ $.Params.User.FirstName }} // accessing a nested field
- The difference is the dot '.' changes value depending on the context
- It starts off pointing at the top level template variable (same as '$')
- The dot changes inside {{ with }} and {{ range }} blocks (only these 2 cases)
- The dollar '$' always points to the top level template variable. It never changes.
5) {{ with <var> }} {{ end }}
- Behaves like an {{ if }}: if <var> is falsy, the block is not executed.
- Otherwise, the dot '.' is set to <var> inside the {{ with }} block.
- If you want to access anything outside of the dot '.', use '$'
{{ with .User.FirstName }}
<p>FirstName: {{ . }}</p>
<p>LastName: {{ $.User.LastName }}</p>
{{ end }}
// Alternative with explicit variable.
{{ with $firstname := .User.FirstName }}
<p>FirstName: {{ . }}</p>
<p>FirstName: {{ $firstname }}</p>
<p>LastName: {{ $.User.LastName }}</p>
{{ end }}
template variable:
{
"User": {
"FirstName": "John",
"LastName": "Doe"
}
}
6) {{ range <var> }} {{ else }} {{ end }}
- Loops over <var>. <var> must be an array, slice, map or channel.
- If there are 0 elements, the {{ else }} block is executed instead.
- The dot '.' is changed to each successive element of the array/slice/map/channel within the {{ range }} block.
- If you want to access anything outside of the dot '.', use '$'
6) {{ range <var> }} {{ else }} {{ end }}
// Basic example
{{ range .Users }}
<p>User: {{ . }}</p>
<p>FirstName: {{ .FirstName }}</p>
<p>Today is {{ $.Today }}</p>
{{ end }}
template variable:
{
"Users": [
{
"FirstName": "John",
"LastName": "Doe"
},
{
"FirstName": "Jane",
"LastName": "Doe"
}
],
"Today": "Thursday"
}
6) {{ range <var> }} {{ else }} {{ end }}
// Alternative with explicit variable.
{{ range $user := .Users }}
<p>User: {{ . }}</p>
<p>User: {{ $user }}</p>
<p>FirstName: {{ .FirstName }}</p>
<p>FirstName: {{ $user.FirstName }}</p>
<p>Today is {{ $.Today }}</p>
{{ end }}
template variable:
{
"Users": [
{
"FirstName": "John",
"LastName": "Doe"
},
{
"FirstName": "Jane",
"LastName": "Doe"
}
],
"Today": "Thursday"
}
6) {{ range <var> }} {{ else }} {{ end }}
// Alternative with $index, $element
// variables. Applies to slices/arrays.
{{ range $i, $user := .Users }}
<p>Index: {{ $i }}</p>
<p>User: {{ . }}</p>
<p>User: {{ $user }}</p>
<p>FirstName: {{ .FirstName }}</p>
<p>FirstName: {{ $user.FirstName }}</p>
<p>Today is {{ $.Today }}</p>
{{ end }}
template variable:
{
"Users": [
{
"FirstName": "John",
"LastName": "Doe"
},
{
"FirstName": "Jane",
"LastName": "Doe"
}
],
"Today": "Thursday"
}
6) {{ range <var> }} {{ else }} {{ end }}
// Alternative with $key, $value
// variables. Applies to maps.
{{ range $key, $value := .User }}
<p>Key: {{ $key }}</p>
<p>Value: {{ . }}</p>
<p>Value: {{ $value }}</p>
<p>Today is {{ $.Today }}</p>
{{ end }}
template variable:
{
"User": {
"FirstName": "John",
"LastName": "Doe",
"Age": 32,
"Address": "2 ABC St.",
"Email": "jd@email.com",
"Username": "jdoe",
},
"Today": "Thursday"
}
Part 3: Functions, methods and pipes
Templates already have some default functions e.g.
- and
{{ if and <cond> <cond> <cond> ... }} {{ end }}
- or
{{ if or <cond> <cond> <cond> ... }} {{ end }}
- not
{{ if not <cond> }} {{ end }}
- len
{{ len <var> }}
// <var> is one of string | slice | array | map | channel
- index
{{ index <slice> <num> ... }}
{{ index <map> <key> ... }}
Templates can call user-defined functions
- You can install new functions into a template by calling .Funcs(map[string]interface{})
template.
New("t").
Funcs(map[string]interface{}{
"greet": func(name string) string { return "hello " + name },
}).
Parse(`{{ greet "bob" }}`)
- Functions can take in any number of arguments but must return one result
func() string
func(s string) string
func(args ...interface{}) interface{}
- You can optionally return an error. If the error is non-nil it will halt template execution
func() (string, error)
func(s string) (string, error)
func(args ...interface{}) (interface{}, error)
Templates can call methods
- Calling a method looks like a field access
type User struct { greeting string }
func (u User) Greet(name string) string {
return g.greeting + " " + name
}
t, _ := template.New("t").Parse(`{{ .User.Greet "bob" }}`)
t.Execute(os.Stdout, map[string]interface{}{
"User": User{greeting: "hello"},
})
Templates can't directly call variables that are functions
- If you are trying to call a variable that happens to be a function, you must use 'call'
type User struct {
Greet func(string) string
}
t, _ := template.New("t").Parse(`{{ call .User.Greet "bob" }}`)
t.Execute(os.Stdout, map[string]interface{}{
"User": User{
Greet: func(name string) { return "hello " + name },
},
})
The results of one function can be piped into another
- When piped, the result of a function is passed in as the last argument to the next function
t, _ := template.
New("t").
Funcs(map[string]interface{}{
"upper": strings.ToUpper,
"greet": func(greeting, name string) string {
return greeting + " " + name
},
}).
Parse(`{{ upper . | greet "hello" }}`) // "hello BOB"
t.Execute(os.Stdout, "bob")
The End
More stuff I didn't cover:
- Templates can be parsed separately and then combined together.
- It's just merging two map[string]*Template-s.
- If two templates combine together, how do their functions interact with each other? Which function wins?
- The functions are just map[string]interface{}-s. They also get merged together.
- html/template will automatically secure HTML/CSS/JS/URL input for you. [1][2]
- even the upcoming {{ break }} and {{ continue }} keywords will respect this. [3]
- Functions must be defined before Parse(). But then you are free to overwrite them after that (why??)
text/template: make and/or operators short-circuit evaluation�https://github.com/golang/go/issues/31103
proposal accepted and CL committed
text/template: add support for optional chaining�https://github.com/golang/go/issues/43608
proposal seems dormant, but optional chaining will be so valuable to have