Functions In Templates—Embrace Or Avoid?
In this spotlight, I want to take a look at a particular feature of Go templates: custom functions, or the FuncMap feature.
How templates work in Go
If you're new to Go, Go's rich standard library provides two templating packages, text/template and html/template. (The latter provides the same interfaces as the former but generates output that is safe against code injection.)
The basic idea of templating is to set up text with placeholders and let the app replace these placeholders with real data.
Let's look at an example. The code below defines a Product
struct type with two fields, and a variable of that type:
type Product struct {
Name string
Price float64
}
(...)
coffee := Product{
Name: "Espresso",
Price: 3.5,
}
Next, it creates a template of name “item”, with placeholders for the struct fields Name
and Price
(note the “dot” notation; the dot represents the context of the template:
tmpl := template.Must(template.New("item").Parse(
`{{.Name}}: {{.Price}}`,
))
Finally, the code executes the template, passing the struct variable coffee
as the context. Execute()
writes the rendered template to an io.Writer
, in this case, os.Stdout
:
tmpl.Execute(os.Stdout, coffee)
// Output: Espresso: 3.5
Adding calculated output
Now let's assume you want to add a discount to the product and also format the price properly as “$n.nn”:
type Product struct {
Name string
Price float64
Discount float64 // e.g., 0.2 for 20% off
}
You could calculate the discounted price, format the price as strings, and fill a new struct
with the result, which you then pass as template context to Execute()
:
coffee := Product{
Name: "Espresso",
Price: 3.5,
Discount: 0.2,
}
// anonymous struct definition for brevity
formattedCoffee := struct {
Name string
OriginalPrice string
FinalPrice string
}{
Name: coffee.Name,
OriginalPrice: fmt.Sprintf("$%.2f", coffee.Price),
FinalPrice: fmt.Sprintf("$%.2f", coffee.Price*(1-coffee.Discount)),
}
tmpl := template.Must(template.New("item").Parse(
`{{.Name}}: {{.FinalPrice}} (was {{.OriginalPrice}})`,
))
tmpl.Execute(os.Stdout, formattedCoffee)
// Output: Espresso: $2.80 (was $3.50)
That's quite a mouthful of additional code for some formatting and a multiplication, isn't it? Imagine you need this in various places of the many templates you have planned to create. Is there a better way?
FuncMap to the rescue
The good news: Yes, there is a better way. You can define functions that you can call from within a template.
Here is how it works:
Instead of the extra struct and the calculations made there, you can define a FuncMap
for the template that contains the functions for discount calculation and price formatting:
funcs := template.FuncMap{
"formatPrice": func(price float64) string {
return fmt.Sprintf("$%.2f", price)
},
"applyDiscount": func(price, discount float64) float64 {
return price * (1 - discount)
},
"taxLabel": func(isTaxable bool) string {
return map[bool]string{true: "(incl. tax)", false: ""}[isTaxable]
},
}
The functions in this map can take arbitrary parameters. At runtime, they must match the actual number of arguments provided. The functions can return one value, or a return value and an error value.
The Template
method Funcs()
injects the functions into the template. Inside the template text, you can call map functions either as func param (...)
, like applyDiscount .Price .Discount
, or through a pipe, like .Price | formatPrice
:
tmpl := template.Must(template.New("item").Funcs(funcs).Parse(
`{{.Name}}: {{applyDiscount .Price .Discount | formatPrice}} (was {{.Price | formatPrice}})`,
))
Applying the template prints out the discounted and original price, formatted as a $ price should be:
tmpl.Execute(os.Stdout, coffee)
// Output: Espresso: $2.80 (was $3.50)
Ok, what did we win?
What did we win, especially—ESPECIALLY!—as template functions have no compile-time type safety? They get evaluated when the template is rendered and may thus fail at runtime, and, what's more problematic, they even fail silently!
Try replacing .Price
in one of the function calls with "one dollar"
. See what I mean?
Still, template functions have undeniable advantages:
- You can reuse logic across the template, and across multiple templates.
- You can simplify template logic. For example, a function
hasPermission
can replace a complicated{{if}}
expression. - Users can write custom templates without recompiling the app.
- Separation of concerns: business logic stays in the Go code while templates can take over the formatting.
A great example for #3 is the static site generator Hugo. Hugo's templating system (based on Go templates) provides several functions that help build flexible templates, no Go coding required.
Surely, you'll still want to minimize the disadvantages from the lack of type safety. Template functions can help here, too. A log
function can implement log.Printf
for your templates:
"log": func(format string, args ...any) string {
log.Printf(format, args...)
return ""
},
Now you can log anything you want from right within your templates:
{{ log "%s: Get it while it's hot!" .Name }}
Template functions definitely increase the fun with templates!