Go · Templates · Embed

Combining templates with embed in Go

Go templates are a great way to build dynamic text or html with Go programs. They are very much like Jinja templates, but they come with a serious shortcoming, they are loaded at runtime, rather than compile time.

This is a problem because a developer has very little control on where the templates will be located on a filesystem when the application is in use (whether in a users machine, or on a cloud).

Go 1.16 added the embed package that meant that files needed by a binary could be embedded in the binary at compile time. This means that a developer now has absolute control on where a template is at compile time, and at runtime.

This is an example of templates being used with embed.

First, the filesystem for this example is thus:

$ tree
.
├── accounts.go
├── cmd
│   └── main.go
├── coreTemplates
│   └── base.tmpl
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile
├── README.md
└──templates
    └── signup.tmpl

The accounts.go file needs to make use of the base.tmpl and signup.tmpl templates, so that is all the go code that needs to be seen.

Note that there is a form in signup.tmpl that requires a CSRF token in it (see the template file pasted at the bottom).

 $ cat accounts.go
package accounts

import (
        "embed"
        "io"
        "log/slog"
        "text/template"

        "github.com/gorilla/csrf"
)

var (
        //go:embed coreTemplates/*.tmpl
        baseTemplateFS embed.FS

        //go:embed templates/*.tmpl
        signupTemplateFS embed.FS
)

type Server struct{}

func NewAccountServer() *Server {
        return &Server{}
}

func (s *Server) ShowRegisterAccountForm(responseWriter io.Writer, csrfToken string) {
        baseTemplate := template.Must(template.New("base").ParseFS(baseTemplateFS, "coreTemplates/base.tmpl"))

        signupTemplate := template.Must(baseTemplate.ParseFS(signupTemplateFS, "templates/signup.tmpl"))

        if err := signupTemplate.ExecuteTemplate(responseWriter, "base.tmpl", map[string]interface{}{
                csrf.TemplateTag: csrfToken,
        }); err != nil {
                slog.Error("unable to execute template", "error", err.Error())
        }
}

The templates are loaded into the embed.FS filesystem, when the file is compiled. I’ve used two embed.FS one for coreTemplates which is a git submodule containing template files that are the basis for system wide web pages, containing common headers, footers, menus, etc.

The second embed.FS contains the templates relevant to this service/domain (for the Domain Driven Design aficionados).

Within the ShowRegisterAccountForm function the base.tmpl is parsed (the path and name of the template being parsed from the embed.FS needs to be supplied).

Similarly the signup.tmpl is parsed, but with the baseTemplate, because we want the signup.tmpl to have some of the base.tmpl attributes.

Finally, the templates are executed with the csrf token injected into the signup.tmpl.

The base.tmpl looks like this:

$ cat coreTemplates/base.tmpl
<html>
  <head>{{block "head" .}} Working Title {{end}}</head>
  <body>{{block "body" .}} Lorem ipsum dolor sit amet, consectetur adipiscing
  elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
  enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
  aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
  voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
  occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
  anim id est laborum.{{end}}</body>
  <footer>{{block "footer" .}}TODO{{end}}</footer>

And the signup.tmpl looks like this.

$ cat templates/signup.tmpl
{{define "head"}}<title>Join Something</title>{{end}}
{{define "body"}}
<form method="post" action="/account">
  {{ .csrfField  }}
  <label for="fname">First name:</label><br>
  <input type="text" id="fname" name="fname" value="Lady"><br>
  <label for="lname">Last name:</label><br>
  <input type="text" id="lname" name="lname" value="Penelope"><br><br>
  <label for="lname">Company:</label><br>
  <input type="text" id="company" name="company" value="International Rescue"><br><br>
  <label for="lname">Email:</label><br>
  <input type="text" id="email" name="email" value="Lady.Penelope@example.com"><br><br>
  <input type="submit" value="Submit">
</form>
{{end}}
Published:
comments powered by Disqus