Illustration created for Convictional, made from the original Go Gopher, created by Renee French.
A lot of Go's initial success as a programming language was defined by projects like Hugo. These projects were able to quickly create powerful templating features by leveraging Go's native language features.
If you’re using Go to build a project that needs to send emails or display web pages (which is most of them), you’re likely utilizing the `html/templating` internal library to make generation of these html bodies easier.
Here at Convictional, we’ve recently made numerous improvements to our templating, which has made our email generation more efficient by orders of magnitude and has significantly improved our pre-release testing.
In this post, we’ll share how we optimized our go template, how we introduced go:embed, and the changes we made to the code for production readiness.
You will be able to find the code for this post here and each section will contain a git tag for the specific part of the codebase at that point.
Standard go template example
First let us put together a standard usage of templating in go. This is what most projects that use templating are going to look at, and what our first implementation of emailing looked like:
This works well and will correctly template in the values we need, but there are a few drawbacks with this method.
First, the template we’re loading in is parsed from the file system each time we want to send an email. This is very inefficient if the email is sent more often than the application restarts.
Second, we have no way to guarantee that the template is there because the email is only sent at runtime. If we’re running this application in a docker container and we incorrectly copy over the files, this email will fail at runtime and may cause issues for our customers.
Finally, in order to use this email package anywhere in the module, we need to use some hacky path manipulation to allow for both local testing and 'external' usage of the email package.
Optimizing our template usage
We can solve the first problem easily by utilizing a lesser-known feature of Go: `init()`. `init()` is a function that automatically runs when a package is imported for the first time. What we can do with this function is initialize all of our templates when the email package is imported for the first time and execute those packages when we need to put data in them.
First, we need to define the template at the top of the email.go file:
Next, we create an `init()` file that parses the template files and assigns that to the variable we just created:
And finally, we will modify our `SendForgotPasswordEmail` function to use this new global variable rather than doing the parsing on it's own:
You can see that the function is simpler now that we have moved the template initialization code out of the function that sends the email. This is faster by two orders of magnitude:
However, there are still a few problems with this approach because we’re still vulnerable to a copying mistake or a deletion of a template file, which can cause runtime errors.We also have to work around where in the filesystem the templates are located.
The `go:embed` directive was introduced alongside the virtual FS proposal in version 1.16. By attaching an absolute route to a variable with the `go:embed` directive, the go compiler will automatically load in the files located at the route into the defined variable. If a file is not found at that location, the compiler will throw an error. Since this is an absolute path, we can also remove the hacky pathing solutions we needed in order to use the email package elsewhere in the module, simplifying our code a lot.
The first step to making this change is to declare some new variables:
Note the `//go:embed` directive above `baseLayoutFS` and `passwordTemplateFS`. In this case rather than embedding the file to a string or byte, we are using the `embed.FS` type. The reason we are doing this is the `html/template` package has native support for parsing `embed.FS` objects.
Next, we need to modify our `init()` function to handle these new variables and ensure that they are properly set up for use:
The rest of `email.go` only needs changes to use the new variables and we now have compile-time guarantees that the files we need for email templating exist and are ready for use.
Running a quick benchmark shows that we haven't lost any performance with this change:
The code used in this post is simplified to make comparisons easier and to illustrate the changes themselves, not any specific structure. In the interest of utility here are some changes that would make this even more robust and easy to use in a production environment.
The first step would be to turn the email package into a library package rather than a domain package. This is somewhat of a stylistic point, but the email package should be handling sending emails, and the domain packages should be handling domain-specific wording and data templating.
To do this, we will move the forgot_password.html template (and it's init code) to the `user` package and create a function `MustParseContentFS` in the email package. `MustParseContentFS` takes in a template from an external package and combines it with the `layout.html` that is still within the `email` package.
Another change is to move the `init()` functions to an `init.go` file in each package. This isn't needed (Go will run it first regardless) but it does make it more obvious that there is some pre-work being done in the package.
You can see what this would look like (plus a few other small tweaks) on a separate branch in the repo for this post.
And with that, we have successfully refactored our template usage to have compile-time guarantees that our templates exist, simplified access from across the project, and optimized the parsing and execution by orders of magnitude.
We hope that this post helps you improve your own email generation in your app. If you’re curious about solving interesting engineering challenges like this one, we’re hiring!