Christian Kilb

Go's template engine: A quick guide

How to work with Go's template engine

It's really great that Go ships with a template engine, so there is no installation of third party libraries necessary.
Still, when I was new to Go it took me a while to find out how to work with it and I didn't find a quick guide that answers most of my questions.
Now, after I managed to work with Go's template engine within several projects, I like to present to you this practical quick guide which will not only tell you how to render templates with multiple pages, sub templates and variable data but also also how to embed your template files into the final binary, so you don't need to copy these to your servers.

Parsing & displaying a template

First things first.
You may already know how to parse and display a template file in Go. For those of you who don't here is a very simple example:

main.go

package main

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

func main() {
	// get the current working directory
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	// parse the template file in the current working directory
	tpl, err := template.ParseFiles(dir + "/template/layout.tmpl")

	// create & start a web server that will render the template
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		err := tpl.Execute(w, nil)

		if err != nil {
			panic(err)
		}
	})

	http.ListenAndServe(":3210", nil)
}

Implementing sub templates

In our template file we might want to include some sub templates.
It makes sense to create sub template files not only to keep your file sizes small but also increase reusability: You can include your sub template file in multiple parents.

How to do so?
First have a look into the base template file:

tpls/layout.tmpl
<html>
    <head>
        <title>Go Template Example</title>
    </head>
</html>
<body>
    <aside>
        {{ template "sidebar" . }}
    </aside>
    <main>
        {{ template "main" . }}
</main>
</body>
</html>

As you can see the file contains a simple HTML document structure. The template code of the sidebar is not included into that file. Instead, a template block with the name sidebar has to be loaded.
If you would run the code above to render that file without any changes you will get a runtime error after you load the page in your web browser:
html/template:layout.tmpl:8:20: no such template "sidebar" Please mind: You will not get a compilation error. Neither you will get an error right after the program has started.
Go will find out that a template is missing after you try to execute the parsed template instance.

So, how to fix it? We have to create the sub templates of course.
Let's do that:

tpls/sidebar.tmpl
{{ define "sidebar" }}
<ul>
    <li>my</li>
    <li>sidebar</li>
    <li>foo</li>
    <li>bar</li>
</ul>
{{ end }} 
tpls/pages/home.tmpl
{{ define "main" }}
    <strong>Welcome to the Home Page.</strong>
{{ end }}

Don't forget to wrap your HTML code with {{ define "template_name" }} and {{ end }}. This will tell Go that the wrapped content is the one that should be loaded in your base template.

After the sub templates have been created, the files have to be parsed, too...

main.go
package main

import (
	...
)

func main() {
	...

	// parse the template file in the current working directory
	tpl, err := template.ParseFiles(dir + "/template/layout.tmpl", dir + "/template/sidebar.tmpl", dir + "/template/page/home.tmpl")

	...
}

And that's how you implement sub templates.
Please mind that the order you pass the template files into the template.ParseFiles method matters. The first passed file should be your base template.

Implementing multiple pages (template slots)

In the example above only the home template will be loaded as the main template.
How can we achieve to support multiple pages? To do so, we have to create multiple template instances - one for each page.
The instances will be saved in a map, indexed by the page name

main.go
package main

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

var tpls map[string]*template.Template

func main() {
	// get the current working directory
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}

	pages := []string{
		"home", "about",
	}

	tpls = make(map[string]*template.Template)

	for _, page := range pages {
		// parse the template file in the current working directory
		tpl, err := template.ParseFiles(dir+"/tpls/layout.tmpl", dir+"/tpls/sidebar.tmpl", dir+"/tpls/pages/"+page+".tmpl")

		if err != nil {
			panic(err)
		}

		tpls[page] = tpl
	}

	// create & start a web server that will render the templates
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		err := tpls["home"].Execute(w, nil)

		if err != nil {
			panic(err)
		}
	})

	// create & start a web server that will render the template
	http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
		err := tpls["about"].Execute(w, nil)

		if err != nil {
			panic(err)
		}
	})

	http.ListenAndServe(":3210", nil)
}
tpls/pages/about.tmpl
{{ define "main" }}
	A few info about me...
{{ end }}

If you restart your Go application, loading / in your web browser will show the layout including your home template.
If you go to /about it will load the about template instead.

Pass data to your templates & sub templates

First we update the home.tmpl file so it's able to display a passed variable:

tpls/pages/home.tmpl
{{ define "main" }}
	Welcome, {{ .name }}!
{{ end }}

Also, we have to pass the name from the main.go file:

main.go

package main

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

var tpls map[string]*template.Template

func main() {
	...

	// create & start a web server that will render the templates
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		err := tpls["home"].Execute(w, map[string]interface{}{
			"name": "Fred",
		})

		if err != nil {
			panic(err)
		}
	})

	...
}

You can see that we now pass a map object as a second argument to the Execute method.
The key type of the map is a string, so it's possible to access the value from the template ({{ .key }}).
The value type is interface{}. This allows any type to be set as a value. interface{} should be avoided in general because it's undermining the benefit of a strongly types programming language.
But, in this case, because the object is only used in the template files and therefore there's no further need of type strictness, it's no big deal.

Let's assume we want to implement a re-usable headline component.
Both of our pages, home and about should make use of this component.
But there's a catch: The home page should show the headline in black color, while the about page should display it in blue. Also, of course, the text of the headline should correspond to the current page.

The first part is a bit tricky:
We have to implement a template helper function which can be used to pass variable data from template to template.
The helper function will have the name dict and it accepts a map object of key type string and any value type.
The dict function will first check if the number of passed arguments is dividable by 2. If not, it will break.
Then, it will create a new map where each argument (key) is mapped to the next argument (value).
This might seem a bit strange at the beginning, but it will simply passing data in sub templates. You will see...

main.go

package main

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


func main() {
	...

    funcMap := template.FuncMap{
		"dict": func(values ...interface{}) (map[string]interface{}, error) {
			if len(values)%2 != 0 {
				return nil, errors.New("invalid dict call")
			}
			dict := make(map[string]interface{}, len(values)/2)
			for i := 0; i < len(values); i += 2 {
				key, ok := values[i].(string)
				if !ok {
					return nil, errors.New("dict keys must be strings")
				}

				dict[key] = values[i+1]
			}
			return dict, nil
		},
	}

    ...

	for _, page := range pages {
		// parse the template file in the current working directory
		tpl := template.New("layout.tmpl").Funcs(funcMap)

		tpl, err = tpl.ParseFiles(dir+"/tpls/layout.tmpl", dir+"/tpls/sidebar.tmpl", dir+"/tpls/pages/"+page+".tmpl", dir+"/tpls/headline.tmpl")

		if err != nil {
			panic(err)
		}

		tpls[page] = tpl
	}

    ...

Unfortunately it's not possible to pass helper functions to the ParseFiles method.
In theory, we could add these helper functions after parsing to the created template instance. But this would be too late. The functions have to be registered before parsing.
That's why we had to create a template instance first (with the name of our base template file layout.tmpl), then add the helper function, then parse the template files.

Now we create a new template headline.tmpl:

tpls/headline.tmpl
{{  define "headline" }}
    <h1 style="font-size: 24pt; font-weight: bold; color: {{ .color }}">{{ .text }}</h1>
{{ end }} 

... and update our home and about template:

tpls/home.tmpl
{{ define "main" }}
{{ template "headline" dict "color" "black" "text" "Home" }}
Welcome, {{ .name }}!
{{ end }}
tpls/about.tmpl
{{ define "main" }}
    {{template "headline" dict "color" "blue" "text" "About me"}}

    A few info about me...
{{ end }}

And that's how you pass variable data from template to template...

Embed templates into your binary

In the examples above we created a folder tpls where we placed all our template files.
The implementation in main.go reads these files relative to the working directory.

So... what happens if you run your application from some other directory? It will fail to load the template files.

Iif you want to deploy your binary application to another server you not only need to make sure that you don't forget about deploying your template files, too. You also need to make sure that you run the application from the right directory.

There's another way though. Go makes it very easy to embed files during compilation. So what we could do instead is to embed our template files into the binary.

How to do so?
First we move our template logic from main.go to another file tpls.go which we place in the same directory where the tmpl files are located:

tpls/tpls.go

package tpls

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

var tpls map[string]*template.Template

func Init() {
	dir, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	pages := []string{
		"home", "about",
	}

	tpls = make(map[string]*template.Template)

	funcMap := template.FuncMap{
		"dict": func(values ...interface{}) (map[string]interface{}, error) {
			if len(values)%2 != 0 {
				return nil, errors.New("invalid dict call")
			}
			dict := make(map[string]interface{}, len(values)/2)
			for i := 0; i < len(values); i += 2 {
				key, ok := values[i].(string)
				if !ok {
					return nil, errors.New("dict keys must be strings")
				}

				dict[key] = values[i+1]
			}
			return dict, nil
		},
	}

	for _, page := range pages {
		tpl := template.New("layout.tmpl").Funcs(funcMap)
		tpl, err := tpl.ParseFiles(
			dir+"/tpls/layout.tmpl",
			dir+"/tpls/pages/"+page+".tmpl",
			dir+"/tpls/sidebar.tmpl",
			dir+"/tpls/headline.tmpl",
		)

		if err != nil {
			panic(err)
		}

		tpls[page] = tpl
	}
}

func Get(page string) *template.Template {
	return tpls[page]
}
main.go

package main

import (
	"ckilb/golang-layout-tpl/tpls"
	"net/http"
)

func main() {
	tpls.Init()

	// create & start a web server that will render the templates
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		err := tpls.Get("home").Execute(w, map[string]interface{}{
			"name": "Fred",
		})

		if err != nil {
			panic(err)
		}
	})

	// create & start a web server that will render the template
	http.HandleFunc("/about", func(w http.ResponseWriter, r *http.Request) {
		err := tpls.Get("about").Execute(w, nil)

		if err != nil {
			panic(err)
		}
	})

	http.ListenAndServe(":3210", nil)
}

This is a bit cleaner because we don't pollute the main.go file with too much template logic.
Now we change the file tpls.go so it will embed the template files.

tpls/tpls.go

package tpls

import (
	"embed"
	"errors"
	"html/template"
)

//go:embed **/*.tmpl *.tmpl
var filesystem embed.FS
var tpls map[string]*template.Template

func Init() {
	pages := []string{
		"home", "about",
	}

	tpls = make(map[string]*template.Template)

	funcMap := template.FuncMap{
		"dict": func(values ...interface{}) (map[string]interface{}, error) {
			if len(values)%2 != 0 {
				return nil, errors.New("invalid dict call")
			}
			dict := make(map[string]interface{}, len(values)/2)
			for i := 0; i < len(values); i += 2 {
				key, ok := values[i].(string)
				if !ok {
					return nil, errors.New("dict keys must be strings")
				}

				dict[key] = values[i+1]
			}
			return dict, nil
		},
	}

	for _, page := range pages {
		tpl := template.New("layout.tmpl").Funcs(funcMap)
		tpl, err := tpl.ParseFS(
			filesystem,
			"layout.tmpl",
			"pages/"+page+".tmpl",
			"sidebar.tmpl",
			"headline.tmpl",
		)

		if err != nil {
			panic(err)
		}

		tpls[page] = tpl
	}
}

func Get(page string) *template.Template {
	return tpls[page]
}

First I removed the usages of os.GetCwd. We don't need the working directory anymore.
Also, instead of parsing the files from the binary file's filesystem using template.ParseFiles I use template.ParseFS which allows me to pass the file system created by Go which represents my embedded template files.

Summary

Working with Go templates can be quite tricky. But once you understand how it works it's a powerful template engine without the need of third party libraries.
It might be a bit more complex in the beginning, especially in comparison to other template engines from other programming languages (like Twig from PHP). But this is mainly due to the fact that Go isn't combining parsing and executing for performance reasons.
Usually template files only have to be parsed once (like, when you start your web server). Once a request comes in the execution (= displaying) of the template can be much faster then.
Also, many other template engines bring a lot of helper functions by default. In Go, you instead have to create and register them yourself. I can't make a decision what I like more. For sure it's helpful if you have a bunch of helper functions from the beginning. Otherwise it's a cleaner approach to only have those functions registered you actually need. Of course there are third party libraries that provide some popular functions - but especially for small functions I prefer to write them myself.

You can find all the example source code here:
https://github.com/ckilb/golang-layout-tpl-example

Contact me

Christian Kilb