Custom template functions
In the last part of this section about templating and dynamic data, I’d like to explain how to create your own custom functions to use in Go templates.
To illustrate this, let’s create a custom humanDate()
function which outputs datetimes in a nice ‘humanized’ format like 1 Jan 2024 at 10:47
or 18 Mar 2024 at 15:04
instead of outputting dates in the default format of YYYY-MM-DD HH:MM:SS +0000 UTC
like we are currently.
There are two main steps to doing this:
We need to create a
template.FuncMap
object containing the customhumanDate()
function.We need to use the
template.Funcs()
method to register this before parsing the templates.
Go ahead and add the following code to your templates.go
file:
package main import ( "html/template" "path/filepath" "time" // New import "snippetbox.alexedwards.net/internal/models" ) ... // Create a humanDate function which returns a nicely formatted string // representation of a time.Time object. func humanDate(t time.Time) string { return t.Format("02 Jan 2006 at 15:04") } // Initialize a template.FuncMap object and store it in a global variable. This is // essentially a string-keyed map which acts as a lookup between the names of our // custom template functions and the functions themselves. var functions = template.FuncMap{ "humanDate": humanDate, } func newTemplateCache() (map[string]*template.Template, error) { cache := map[string]*template.Template{} pages, err := filepath.Glob("./ui/html/pages/*.tmpl") if err != nil { return nil, err } for _, page := range pages { name := filepath.Base(page) // The template.FuncMap must be registered with the template set before you // call the ParseFiles() method. This means we have to use template.New() to // create an empty template set, use the Funcs() method to register the // template.FuncMap, and then parse the file as normal. ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl") if err != nil { return nil, err } ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl") if err != nil { return nil, err } ts, err = ts.ParseFiles(page) if err != nil { return nil, err } cache[name] = ts } return cache, nil }
Before we continue, I should explain: custom template functions (like our humanDate()
function) can accept as many parameters as they need to, but they must return one value only. The only exception to this is if you want to return an error as the second value, in which case that’s OK too.
Now we can use our humanDate()
function in the same way as the built-in template functions:
{{define "title"}}Home{{end}} {{define "main"}} <h2>Latest Snippets</h2> {{if .Snippets}} <table> <tr> <th>Title</th> <th>Created</th> <th>ID</th> </tr> {{range .Snippets}} <tr> <td><a href='/snippet/view/{{.ID}}'>{{.Title}}</a></td> <!-- Use the new template function here --> <td>{{humanDate .Created}}</td> <td>#{{.ID}}</td> </tr> {{end}} </table> {{else}} <p>There's nothing to see here... yet!</p> {{end}} {{end}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}} {{define "main"}} {{with .Snippet}} <div class='snippet'> <div class='metadata'> <strong>{{.Title}}</strong> <span>#{{.ID}}</span> </div> <pre><code>{{.Content}}</code></pre> <div class='metadata'> <!-- Use the new template function here --> <time>Created: {{humanDate .Created}}</time> <time>Expires: {{humanDate .Expires}}</time> </div> </div> {{end}} {{end}}
Once that’s done, restart the application. If you visit http://localhost:4000
and http://localhost:4000/snippet/view/1
in your browser you should see the new, nicely formatted, dates being used.


Additional information
Pipelining
In the code above, we called our custom template function like this:
<time>Created: {{humanDate .Created}}</time>
An alternative approach is to use the |
character to pipeline values to a function. This works a bit like pipelining outputs from one command to another in Unix terminals. We could re-write the above as:
<time>Created: {{.Created | humanDate}}</time>
A nice feature of pipelining is that you can make an arbitrarily long chain of template functions which use the output from one as the input for the next. For example, we could pipeline the output from our humanDate
function to the inbuilt printf
function like so:
<time>{{.Created | humanDate | printf "Created: %s"}}</time>