Honest, loyal, senior software engineer, looking to make a real difference by putting people first.
Working with Go templates, being a compiled language, is not always an easy task. And I have definitely had my fair share of gnashing of teeth in past which you can easily refer to here and here.
So when I started a new Go project that required a web view component, I decided to take my past learnings and create a public package that will hopefully, remove most of the hurt from your rendering needs too.
You can directly view the new package here, but I recommend you continue to read on.
What I needed from a templating package was the ability to
My requirements really aren’t that out of the ordinary, but even with a simple Google search for an existing package, the results fell well short of expectations. There is of course a great package from nicksnyder/go-i18n to render i18n replacement “values”, but this is a hugely different requirement to the full rendering of templates.
With my requirements defined, it was important to note and address some of the (IMO) shortcomings with the default Go templating package. As of writing go version go1.12.1
was in use to demonstrate.
From the surface, this func looks great. Pass a glob pattern to your source *.html files and have each added to a pointer template instance. Regrettably, the reality is not so great. Take for example a folder structure of
templates
├── _footer.html
├── _nav.html
├── en
│ └── index.html
├── error.html
├── es
│ ├── error.html
│ └── index.html
└── layout.html
// Calling with the below ParseGlob
all := template.New("")
all.ParseGlob("templates/*.html")
all.ParseGlob("templates/*/*.html")
for _, t := range all.Templates() {
fmt.Println(t.Name())
}
// Will output (similar)
_footer.html
_nav.html
error.html
index.html
layout.html
Notice something alarming here? Nested directories cannot be referred to and we are missing x2 templates! We are missing the template names of error.html
and index.html
. This is because the template.ParseGlob
func does not respect template paths and overwrites templates of the same name. As it turns out, the last duplicate template name wins here. With this knowledge in hand, we can handle this unexpected behaviour.
Addtionally, you are unable to use a variable string as the name
for the builtin template
action. This means you have to provide an explicit static string value to refer to an embbeded template inside your template. For example
proof := template.New("proof")
template.Must(proof.Parse(`{{template .variable_template_name .}}`))
// Will output (similar)
panic: template: proof:1: unexpected ".variable_"… in template clause
As helpful as it might be to programatically provide a template name at template execution, this is not something the standard Go templating package allows. If you want more information as to why, you can refer to Rob Pikes' explanation here.
To help overcome the shortcomings and to meet the above requirements, I am pleased to announce gitlab.com/kylehqcom/stencil - a Go templating package that has strong opinions with easy extensibility.
In brief, Stencil is made up primarily of x3 interfaces, a Loader, a Matcher and an Executor. These interfaces are easily extensible for your applications needs. However, the real power of the package comes into its own when you create your own Decorators.
Stencil comes bundled with a Request Decorator, which IMO, will cover 95% of your rendering needs. Render Locale specific templates (including error templates) with fallbacks when a specific is not found. You have no need to worry about missing duplicate named templates and you also get the bonus extra of built in logic for Flash messages. Rather than turn this blog post into another README.md, please feel free to visit or check out and use gitlab.com/kylehqcom/stencil in your own time.
Logic aside, the hardest challenge I had with this package was trying to determine my interfaces for you/me, the reader/user. Personally, I desperately wanted to create a “Render” interface. I looped through numerous iterations of interface, method and struct/field names. Sometimes, at crazy morning hours. Basically the ceiling became a new friend… I guess because I knew from the outset that I wanted to make this a public standalone Go package, I personally felt the weight of a thousand eyes upon me…
These days however, I have a little time/wisdom on my side and I realised that trying to enforce a Render
interface would be nothing short of a terrible abstraction.
Instead, it is far better to provide extensibility into your code, to give your users the freedom to use your tools in ways you likely never expected or even thought possible.
If you’ve made it this far, then I thank you! Now with Stencil in the public domain, I hope you take Stencil for a test drive for yourself and let me know of your own experiences. I’m always available to take feature requests, bug reports, improvements and of course criticisms too. It all helps in becoming a better developer person. The learning never stops @kylehqcom.