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}}