Go Templating Tips

Series: golang-bits

Index #

Introduction #

In the past I had a project in which we used templ. It's a templating library (with a terrible name for search engines) that is quite useful and (in my opinion) intuitive. It works in a similar way to sqlc in that it generates go code from a different source language. The reason we used it at the time was because the go stdlib seemed to be lacking power or ease of use. I can't and won't comment on this since those are quite subjective measures, but what is not a subjective measure is that adding templ adds another dependency and since I like to minimize dependencies, I've tried to make due with go's standard library templating. I think you'll get a lot of mileage out of the standard library if you understand how to use it.

Go has two templating libraries that are practically the same. One is text/template and the other one is html/template. They're both equal in usage but the html one has some html sanitization. Going forward I'll treat them as the same since they only differ in details.

I also recommend that you read the docs for at text/template. The docs mention how the templating engine works but not how you should organize your templates or how to use the library. That is understandably use-case dependent and up to the reader. I'm just going to share my experience using it. Got it? Okay, let's go!

Using Templates Like Functions #

One of the most useful features of templ is that it enabled you to call other templates via go functions. And though you can't do that using actual functions using Go's templating, you can still do that using the template action. This would be useful in cases where you want to re-use some component. Here's how that would look like:

// This is how a post looks like
type Post struct {
  Title       string
  Description string
  Content     string
  //...
}

// First we define a template for rendering a post
{{ define "post-entry" }}
<div class="item">
  <h3>{{ .Title }}</h3>
  <div>{{ .Description }}</div>
  <p>{{ .Content }}</p>
</div>
{{end}}

// Next we have the call of the template in a loop. We loop over all Posts using
// `range .Posts` which will set '.' inside the loop body to a post. We can then
// call the template and pass it a single post
{{ range .Posts }}
  {{template "post-entry" .}}
{{ end }}

Here's a complete example:

package main

import (
 "fmt"
 "os"
 "text/template"
)

type Post struct {
 Title       string
 Description string
 Content     string
}

var (
 tmpls = `{{ define "post-entry" }}
<div class="item">
  <h3>{{ .Title }}</h3>
  <div>{{ .Description }}</div>
  <p>{{ .Content }}</p>
</div>
{{end}}

{{ range .Posts }}
  {{template "post-entry" .}}
{{ end }}`
)

func main() {
 posts := struct {
  Posts []Post
 }{
  []Post{
   {
    Title:       "Post 1",
    Description: "Description 1",
    Content:     "Lorem Ipsum",
   },
   {
    Title:       "Post 2",
    Description: "Description 2",
    Content:     "Gotta gopher fast",
   },
  },
 }

 t := template.Must(template.New("base").Parse(tmpls))
 err := t.Execute(os.Stdout, posts)
 if err != nil {
  fmt.Println(err.Error())
  os.Exit(1)
 }
}

Link to the same example but on the go playground.

In my website I only have one re-usable component at the time of writing but it's still very useful since I only have to design and style one component and know that it will work on all pages that I use it in :)

Overwriting Page Components #

Two common approaches to design you pages are composing and slot-based composition. In the first one you define components like header, footer, and then compose your pages using them:

{{ template "header" }}

// Action here
{{ range .Posts }}
  // ...
{{ end }}

{{ template "footer" }}

At the time of writing I structured my blog using the second approach. I have one base layout and all pages just slot their components into that layout. In Go standard library templates you would do that using 'blocks' and re-defining those blocks in the template you want to render.

// layout.tmpl
<header>...</header>

{{ block "content" . }}
<p>This defines the slot which will later be overwritten</p>
{{ end }}

<footer>...</footer>

// post-list.tmpl
// When rendering the post list then you only have to define the "content" part
// and it will overwrite the one in the base layout when rendering

{{ define "content" }}

{{ range .Posts }}
  // ...
{{ end }}

{{ end }}

The only thing you then have to do is to parse the templates in the correct order: first layout, then post-list.

The neat thing about this approach is that you can use structs that conform to a 'page definition' in an implicit way, meaning that you can use any struct as long as it has the common set of fields (Title and Description for example). The content rendering is up to the page since it overwrites the content slot and you only need a single execution pass to render your page. This approach is tremendously flexible and efficient even if it is not very intuitive.

I've prepared once again a full example:

package main

import (
 "os"
 "text/template"
)

type Post struct {
 Title       string
 Description string
 Content     string
}

var (
 // This is the base layout. As you can see it has a header, then it always
 // accesses the `Title` field, has a "content" block and then a footer. All
 // pages should have a Title that want to use this layout. If not it will not
 // be printed
 layout = `
<header>...</header>

{{ if .Title }}
{{ .Title }}
{{ end }}

{{ block "content" . }}
content here
{{ end }}

<footer>...</footer>`

 // Template for a single post. Note that posts have a title so that will be
 // used by the template above
 postTmpl = `
{{ define "content" }}
{{ .Title }}

{{ .Description }}

{{ .Content }}
{{end}}`

 postlistTmpl = `
{{ define "content" }}
 {{ range .Posts }}
 Title: {{ .Title }}
 Desc: {{ .Description }}
 {{end}}
{{end}}`
)

// NOTE: I've chosen to ignore errors (or panic if they arise) rather than
// handle them since I want to focus on the example. Please handle errors when
// using this!
func main() {
 posts := struct {
  // Since the post list does not have a title indirectly it needs to be given
  // one
  Title string
  Posts []Post
 }{
  "Post list",
  []Post{
   {
    Title:       "Post 1",
    Description: "Description 1",
    Content:     "Lorem Ipsum",
   },
   {
    Title:       "Post 2",
    Description: "Description 2",
    Content:     "Gotta gopher fast",
   },
  },
 }

 t := template.Must(template.New("layout").Parse(layout))
 p, _ := template.Must(t.Clone()).Parse(postTmpl)
 plist, _ := template.Must(t.Clone()).Parse(postlistTmpl)

 // Here we should set a title but since the template allows no title to be
 // given it will just be omitted
 t.Execute(os.Stdout, nil)

 // This would cause an error though since it is a struct and will try to
 // evaluate the `Title` field
 //t.Execute(os.Stdout, struct{}{})

 p.Execute(os.Stdout, posts.Posts[0]) // will use post title
 plist.Execute(os.Stdout, posts)      // title has been set explicitly
}

Same thing on the go playground. Also read the stdlib example!

For this approach I've written two small functions that make usages really simple. And yes, the Webserver has an embedded fs.FS...it's not pretty but it get's the job done.

// Utility function to render the given data with the cascaded templates into w
func (t *Webserver) Render(w io.Writer, data any, templates ...string) error {
 fsys, err := fs.Sub(t.content, templateDir)
 if err != nil {
  return fmt.Errorf("error getting templates dir: %v", err)
 }

 return RenderFS(fsys, w, data, templates...)
}

// Renders the data using templates into w. Datasource for templates is filesystem
func RenderFS(f fs.FS, w io.Writer, data any, templates ...string) error {
 if len(templates) == 0 {
  return errors.New("no templates specified")
 }

 tmpl, err := template.New(templates[0]).Funcs(funcs).ParseFS(f, templates...)
 if err != nil {
  return fmt.Errorf("error parsing templates: %v", err)
 }

 return  tmpl.Execute(w, data)
}

And then use it like this:

err := h.Render(w, overview, TmplLayout, TmplComponents, TmplBlogIndex)
// ...

That's it! Hope you learned something new!

References #