Go Templates - a final outcry

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.

My requirements

What I needed from a templating package was the ability to

  1. Render locale specific templates
  2. Have a sensible template fallback if a locale specific template did not exist
  3. Be able to use a shorthanded naming convention rather than full path names
  4. Have integrated Flash messages that would render if present
  5. Be easily extensible should I need to load & parse from multiple sources

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.

Some Go template shortcomings (IMO)

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.

1. *template.ParseGlob()

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.

2. {{template .variable_name . }}

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.

Stencil - a Go Templating Package

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.

Interfaces for public consumption

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.

Wrap up

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.

Another year of interest?

Wow….it’s been quite “the” year.

At the LGBTQ party hosted by the US Embassy in Mexico

I’m not sure how most measure their years??? I measure mine not only by time, but by distance as well. We just need to to define our distance metric =]

So what has changed?

  • Well, I no longer fear or doubt myself. That’s a biggie!
  • Opinions are just that.
  • Social media aghhh…reach out rather than opt in!
  • I want to be a father.
  • I want a dog (cats can hang too).
  • I am a global citizen, start with you local community.

There has been one particular thing that did happen however that has been more than “quite” amazing.

I took a chance! That’s always scary of course….but isn’t that what life is all about?

Living the dream?

It hasn’t all been rose coloured glasses though. I’m finding myself mostly frustrated these days.

What’s worse, I feel like I’m clasping at my last straws of empathy….well….I don’t know that I totally agree with that.

One of the things that keeps me feeling human at this point in time is selfless giving. I don’t mean the selfless giving that is unhealthy, I mean the giving that made me empty my pockets to the family living in their street truck. It seems like such a simple gesture, but I would much rather actually see the smile, interact with the human, than have my bank account auto send a donation “somewhere”.

Regardless of those giving glimmers, in the daily drone, that daily hum, I do find myself thinking that I’m surrounded by completely clueless people. Trouble is, I know that “that” cannot be true.

I’m also now feeling more than ever, the constant tick of the clock. A constant that reminds me that I am on the slow burn home…and that tick only continues to get louder. Finding time seems to be the hardest thing of all. I mean, my last blog post was April…it’s November FFS and I’m still trying to find time to finish this! It’s not even about shifting priorities anymore!

And I guess there is still one thing lingering that I need to put to rest.

He is my dad.

It’s been years since we last spoke.

My father taught me that “Manners, maketh the man”, and that “Patience is a virtue”, trouble is we have reality to contend with.

My father is a drunk.

My father physically abused and raped my mum.

Lets just say it’s a difficult delimma distinguishing values.

He is now 72 years of age and I know there can’t be “too” much gas left in the tank. I’ve been replaying and repeating, how this next interaction will play out. What I would say? How would I confront? Would I even confront him at all? But I recently had some good advice, the next interaction is not about him at all. The next interaction is entirely about me.

“Dad, I am no longer scared of you, I do not live in fear. I didn’t make your choices. I am not a mistake.

I’m just me Dad, and I don’t need to prove that to you anymore.”

I’m not sure how most measure their years, I guess the question is, what is your metric?

Sad beautiful truths

Serendipity played it’s part yesterday. A series of events that lead me to an open invitation track day at Hampton Downs. With summer drawing to a close here in New Zealand, and my bike going back into storage due to overseas committments, “scratching” on a track day was the perfect way to end the season.

But it wasn’t the thrill of the 20 somethings on their sports bikes being left in my wake on my “tourer” that I enjoyed most. Nor the power wheelies leaving the apex on the hair-pin turns. Or even the circuit scrutineer on his prupose built track bike letting me know after the session he had trouble catching me. What I enjoyed most was the 60 something year old widower I befriended in the pits.

Unbeknownst to most, motorcyclists are a friendly, supportive and curious bunch. It’s the same reason we always wave to one another on the road, to uphold and respect the unwritten code. Maybe it was the thought of me in years to come, or that I’m always eager to learn, but this day I pulled up beside a grey and aged owner, kitted in his suit, supping on a cuppa tea.

With a purpose built Triumph track bike, a van full of spares and all the racing kit to see you run an entire season, I was surpised when he metioned he was heading out in the “slow” group. I know full well that age isn’t a limiting factor on the back of a motorcycle and have certainly been “schooled” for thinking so. I enquired as to why and he replied.

“This is the first time I have been back out on the track since my wifes’ death. I took a year and a half off to nurse her until her passing.”

He told me that she had cancer of the throat so was unable to speak. A hole cut in her larynx to allow an airway to breathe. He feed her directly via a tube to the stomach.

“She would be here now.” he said, “She would always come to my track meets, we travelled the country together, just us and these two wheels.”

As the day progressed, I would confirm his last session, how he was going. Was he finding his rhythm, or getting obstructed by other riders? We would exchange tips, entry and brake reference points, preferred lines… and then there was a silence.

For whatever reason, I don’t really know why, he decided to tell me the last written words his dying wife wrote.

“I love you. Kiss, kiss. Hug, hug”

He said he didn’t really think much of it at the time, replying of course “I love you too”. He regrets not properly saying goodbye as she passed later that evening.

A tear rolled down my cheek.

With his words on my mind, I headed out for my next session, eager in some sense to get back to my new friend.

He didn’t finish the remainder of the day. I guess he knew he really didn’t need to. Packing up early, we left each other with a handshake and a hat tip goodbye.

There’s something to be said for any being who loses their life partner, to stand up, to get up, to get out and do the things that brought them so much enjoyment in past.

These poignant reminders, that none of us have very long within this realm.

Personally, I’m choosing a life without fear nor regrets.

MyDigest - A learned journey

Luckily and happily, I’m always in an ongoing pursuit to level up my knowledge. For me personally, I’ve found the best way I learn is not by reading, but by building instead. With that, I present to you my latest online application:

Starting simple and small, MyDigests’ current purpose is to deliver a daily sales digest to retailers using the Vend POS Retail platform. We have much larger future goals, but we plan to grow with the feedback of our users into different data sources and broadcasting mediums. So far it’s been an interesting journey. Without a doubt, I’m an engineer not a product manager!

Although the techs enclosed aren’t necessarily bleeding edge in all circumstances, good tech does not stay static long! And more importantly, why be good with a technology when you can have a mastery instead?

By no means a definitive list (it will most likely grow) please stay tuned as I share my learnings on:

  • Why Golang?
  • An Application Structure
  • Mongo Much?
  • Schedules and Queues
  • API JSON Spec
  • Frontend Folly
  • Templating
  • Google Cloud vs Amazon
  • Kubernetes

Hopefully my learnings can be used as a guide, and if nothing else, I hope you’re inspired to build something for yourself too! Have a specific request or question? Please reach out and I will gladly oblige where able.

Golang Templates - An Addendum

Dealing with DOM Ready.

Note: If you haven’t been playing the at home game, then I suggest you read the first post here, otherwise let’s assume that you have a directory structure like the following.

main.go
templates
├── _footer.html
├── _header.html
├── layout.html
├── templates.go
└── user
    ├── dashboard.html

The problem: ensuring that any JS snippets defined throughout your application templates are executed with a valid DOM available. Granted, it would be best practice for you to combine all of your .js files and then load the src in the head using the async attribute. But even a snippet in the _header.html may still fail without jQuery loaded.

We can do better.

Let’s collect any/all js snippets from any/all of the available templates and then render them at the last possible moment in the DOM.

template.FuncMap

First off we will create an accessible template function to pass our js snippets.

jsSnippets := []template.JS{}
tplFuncs := template.FuncMap{
    // This template method just appends js snippets. The
    // Render() method is responsible for concatenation and
    // adding to the base layouts content.
    "domReady": func(js string) string {
        jsSnippets = append(jsSnippets, template.JS(js))
        return ""
    },
}

A couple things to note, any function in a template.FuncMap must return either a single value or x2 values where the 2nd is of type error. As we are not concerned with the return value here, we just return an empty string.

The purpose of the function is to store your js string snippets as a template.JS value for future rendering. Then in your template files call

// The use of back ticks `s removes any single/double
// quote shenanigans.
{{domReady `console.log("All of the JS")`}}

First failures.

Ok so now we have our snippets gathered from our templates, we just need to pass them up to our outer layout…..ummm sorry? Let’s recap on our render method.

func Render(.....) {
    // At first we thought that we can iterate over
    // our js snippets now and assign to the template
    // data. But it's always empty!
    var domReadyJS template.JS
    if 0 < len(jsSnippets) {
        for _, jq := range jsSnippets {
            domReadyJS = domReadyJS + jq
        }
    }
    data["domReadyJS"] = domReadyJS
    
    layoutClone, _ := GetLayout(l).Clone()
    layoutClone.Funcs(funcs)
    err := layoutClone.Execute(w, data)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        fmt.Fprintf(w, errorTemplate, name, err.Error())
    }
}

My first attempt to implement of course failed due to a chicken/egg scenario. When the Render() method is called, the layout will always have an empty slice of js snippets! This is because it’s not until the enclosed named template is executed that any calls to the domReady function will be invoked.

You can always get out!

If you were thinking about how to hook into the templates to pass values to the layouts, take it from me, don’t. I tried. The better solution is of course to buffer the content of the layout, check for js snippets and then write the results to an outer template.

For a full example, refer to the this snippet - https://gitlab.com/snippets/1663919

With this logic, you can call {{domReady}} multiple times from any layout/template and have the results combined and rendered at the base of the page. Much better!

PS: In case you’re wondering, the chicken ALWAYS came before the egg.

It's where the magic happens © 2019 - KyleHQ