You may want to inform yourself about human rights in China.

On a Pool of Go Text/Template

date: 2023-07-31
update: 2023-08-15

The goal here is to see how:

  1. to load a first batch of templates from a toolbox/ directory,
  2. and how to use them while iterating on a second set of templates.

As a syntactical refinement, we’ll see how to call the first batch of templates from the second one as functions. Meaning, if you have a template toolbox/tool.tmpl, we’ll see how to use it from the second set of templates as:

{{ tool arg0 arg1 }}

The first batch of templates provides generic functionality to be used throughout the second batch. A typical use case would be a static site generator, where you have some generic templating functions in toolbox/*.tmpl, that you can use on all your pages/*.tmpl. I’ll keep referring to this use-case later on to help clear things out.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain. by Reinhold MΓΆller through wikimedia.org – CC-BY-SA-4.0

Getting started

Let’s start by simply calling a template with some arguments, wrapped in a map[string]any. The pros of using such a generic map is that the data can be arbitrarily structured (random JSON file for example): the Go code doesn’t have to know how to precisely unwrap it into precisely-typed data structures. Of course, the cost of this extra-flexibility is a loss of static checks.

In the present article, it’ll make the code slightly more succinct, so I’ll stick with it.

package main

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

func main() {
	err := template.Must(
		template.ParseFiles("pages/00.tmpl"),
	).Execute(os.Stdout, map[string]any{
		"user" : map[string]any{
			"name" : "earthling",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
# stdout
hello, earthling!

This should be fairly self-explanatory; the details are best explained in the relevant documentation:

Adding and using some tools.

We now need to parse two sets of templates; mind the use of template.ParseGlob() to load the set of tools:

package main

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

func main() {
	err := template.Must(
		template.Must(
			template.ParseFiles("pages/01.tmpl"),
		).ParseGlob("toolbox/*.tmpl"),
	).Execute(os.Stdout, map[string]any{
		"user" : map[string]any{
			"name" : "earthling",
		},
	})

	if err != nil {
		log.Fatal(err)
	}
}
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello, earthling!

Note: toolbox/show.tmpl is loaded but unused.

There’s one important subtlety in the previous code: a template.Template is a wrapper that can actually refer to multiple templates. If you try to template.Execute() it, the execution entry point will be the first loaded template, in our case, pages/01.tmpl.

An easy mistake would have been to load first the toolbox/*, then a page, and to be left slightly befuddled, as one of the toolbox/ template would have been executed instead.

package main

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

func main() {
	// XXX: This is usually NOT what you want
	err := template.Must(
		template.Must(
			template.ParseGlob("toolbox/*.tmpl"),
		).ParseFiles("pages/01.tmpl"),
	).Execute(os.Stdout, map[string]any{
		"user" : map[string]any{
			"name" : "earthling",
		},
	})

	if err != nil {
		log.Fatal(err)
	}
}
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello
Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain. by Diego Delso (1974 - ), aka “Poco a poco” through wikimedia.org – CC-BY-SA-4.0

One step further

Of course, considering the plan for this article, we want a workaround, as we’ll want to first load the toolbox, then the “pages”: while template.Execute() by default calls the first loaded template, there’s a template.ExecuteTemplate() executing a specific template from the set.

package main

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

func main() {
	err := template.Must(
		template.Must(
			template.ParseGlob("toolbox/*.tmpl"),
		).ParseFiles("pages/01.tmpl"),
	).ExecuteTemplate(os.Stdout, "01.tmpl", map[string]any{
		"user" : map[string]any{
			"name" : "earthling",
		},
	})

	if err != nil {
		log.Fatal(err)
	}
}
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello, earthling!

Iterating on the pages

Let’s add an init() function to load the toolbox, and let’s iterate on some pages. Note that in real life scenario, this list would have been given e.g. via a filepath.Walk():

package main

import (
	"log"
	"os"
	"path/filepath"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.Must(
		template.ParseGlob("toolbox/*.tmpl"),
	)
}

func main() {
	for _, x := range []string{"00.tmpl", "01.tmpl"} {
		err := template.Must(
			template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"user" : map[string]any{
				"name" : "from "+x,
			},
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello, from 00.tmpl!

hello, from 01.tmpl!

Calling Go code from a template

Here’s a small intermediate step showing how to register functions to a template.Template so as to call them from the templates:

package main

import (
	"log"
	"os"
	"path/filepath"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.Must(
		template.ParseGlob("toolbox/*.tmpl"),
	).Funcs(template.FuncMap{
		"doGreet" : func() string {
			return "hi"
		},
	})
}

func main() {
	for _, x := range []string{"00.tmpl", "01.tmpl", "05.tmpl"} {
		err := template.Must(
			template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"user" : map[string]any{
				"name" : "from "+x,
			},
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* pages/05.tmpl */}}
{{- doGreet }}, {{ .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello, from 00.tmpl!

hello, from 01.tmpl!
hi, from 05.tmpl!

There are of course some restrictions regarding what kind of functions you can register, especially concerning their return value. The documentation is rather clear on all those aspects.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain. by Kroll Markus through wikimedia.org – CC-BY-SA-4.0

Toolbox templates called as functions

Here’s the syntactic sugar: the idea is simply to iterate on all the templates you’ve just loaded from the toolbox/, and to register one function for them.

We need to trim away the .tmpl prefix, and more generally, to make sure we can build a proper function name from the (toolbox) template path. Of course, choosing appropriate template names in the first place is the best way to Go.

Mind the use of a strings.Builder to catpure the output of a template execution to a string.

package main

import (
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.Must(
		template.ParseGlob("toolbox/*.tmpl"),
	).Funcs(template.FuncMap{
		"doGreet" : func() string {
			return "hi"
		},
	})
	for _, x := range tmpls.Templates() {
		n := strings.TrimSuffix(x.Name(), ".tmpl")
		// beware of the race...
		m := x.Name()
		tmpls.Funcs(template.FuncMap{
			n : func(ys ...any) (string, error) {
				var s strings.Builder
				err := tmpls.ExecuteTemplate(&s, m, ys)
				return s.String(), err
			},
		})
	}
}

func main() {
	for _, x := range []string{"00.tmpl", "01.tmpl", "05.tmpl", "06.tmpl"} {
		err := template.Must(
			template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"user" : map[string]any{
				"name" : "from "+x,
			},
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/00.tmpl */}}
hello, {{ .user.name }}!
{{/* pages/01.tmpl */}}
{{- template "greet.tmpl" }}, {{ .user.name }}!
{{/* pages/05.tmpl */}}
{{- doGreet }}, {{ .user.name }}!
{{/* pages/06.tmpl */}}
{{- greet }}, {{ show .user.name }}!
{{/* toolbox/greet.tmpl */}}
hello
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello, from 00.tmpl!

hello, from 01.tmpl!
hi, from 05.tmpl!

hello, from 06.tmpl!

Syntactic sugar

Lucky for us, the templates from the toolbox/ don’t call each other. But they could. However, if we tried to call them as functions, we’ll get a parsing error when loading the toolbox/, because the functions haven’t been registered just yet. But we can still call them using the predefined template function. However, by default, this template function only allows to send one “argument” to the template (the usual “pipeline”), but in practice, you may have want to send more than one.

We can tweak things by adding an arr() function to the template.Template that will collapse multiple argument to a single array, hence to a single argument/pipeline.

Note also that so far, doGreet() was only used in the pages, and so it was OK to load it after the toolbox, but we now need arr() within the toolbox, so we have to define at least arr() before loading the toolbox. We achieve this by creating a dummy template first. This yields something like:

package main

import (
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.Must(template.New("").Funcs(template.FuncMap{
		"doGreet" : func() string {
			return "hi"
		},
		"arr" : func(xs ...any) []any {
			return xs
		},
	}).ParseGlob("toolbox/*.tmpl"))

	for _, x := range tmpls.Templates() {
		n := strings.TrimSuffix(x.Name(), ".tmpl")
		// beware of the race...
		m := x.Name()
		tmpls.Funcs(template.FuncMap{
			n : func(ys ...any) (string, error) {
				var s strings.Builder
				err := tmpls.ExecuteTemplate(&s, m, ys)
				return s.String(), err
			},
		})
	}
}

func main() {
	for _, x := range []string{"07.tmpl"} {
		err := template.Must(
			template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"user" : map[string]any{
				"name" : "from "+x,
			},
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/07.tmpl */}}
{{ callshow }}
{{/* toolbox/callshow.tmpl */}}
{{ template "show.tmpl" (arr "hello" " " "from" " " "callshow") }}
{{/* toolbox/show.tmpl */}}
{{- range . -}}
	{{- . -}}
{{- end -}}
# stdout

hello from callshow

We should actually be able to call the toolbox/ templates from each others as functions. Indeed, consider the following example, where we first define a function test, load the toolbox, and redefine this function test. When calling it from he toolbox later on, it’s using this last definition:

package main

import (
	"log"
	"os"
	"path/filepath"
	"strings"
	"text/template"
)

var tmpls *template.Template

func init() {
	tmpls = template.Must(template.New("").Funcs(template.FuncMap{
		"doGreet" : func() string {
			return "hi"
		},
		"arr" : func(xs ...any) []any {
			return xs
		},
		"test" : func() string {
			return "before"
		},
	}).ParseGlob("toolbox/*.tmpl"))

	for _, x := range tmpls.Templates() {
		n := strings.TrimSuffix(x.Name(), ".tmpl")
		// beware of the race...
		m := x.Name()
		tmpls.Funcs(template.FuncMap{
			n : func(ys ...any) (string, error) {
				var s strings.Builder
				err := tmpls.ExecuteTemplate(&s, m, ys)
				return s.String(), err
			},
		})
	}

	tmpls.Funcs(template.FuncMap{
		"test" : func() string {
			return "after"
		},
	})
}

func main() {
	for _, x := range []string{"08.tmpl"} {
		err := template.Must(
			template.Must(tmpls.Clone()).ParseFiles(filepath.Join("pages", x)),
		).ExecuteTemplate(os.Stdout, x, map[string]any{
			"user" : map[string]any{
				"name" : "from "+x,
			},
		})

		if err != nil {
			log.Fatal(err)
		}
	}
}
{{/* pages/08.tmpl */}}
{{ calltest }}
{{/* toolbox/calltest.tmpl */}}
{{ test }}
# stdout

after

Exercise: Registering dummy functions for each toolbox/*.tmpl before updating them with the actual functions, and testing it all, is left as an exercise to the reader1.

At this point, you may want to consider defining some regular interface between your templates/toolbox, depending on what kind of data you originally intend to provide your pages/ with: an array is nice, but a hash with some predefined entries might be a better option.

Note: In case you’re interested, I’ve got another article building on this kind of template structures, allowing to pipe the content of a template from/to an arbitrary command.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain.

Temple of Debod, in a reflecting pool of the Parque del Oeste park, Madrid, Spain. by Jiuguang Wang through wikimedia.org – CC-BY-SA-2.0


  1. To be honest, I’m being lazy; I hope it works as advertised. ↩︎


Comments

By email, at mathieu.bivert chez:

email