Templating libraries are an essential part of a programming language ecosystem. Go’s standard library provides powerful templating libraries out of the box, but there are also multiple community-built templating libraries in Go for developers to use.
With several choices available, it can be difficult to select the best choice for your particular use case.
This article will compare some of the most popular templating libraries based on performance, functionality, and security, using Go’s benchmarking tool. Specifically, we will look at some common operations — like conditional operations, template nesting, and loops, etc. — used in templates and benchmark them to give you an idea of each template engine’s performance and memory footprint.
I’ve decided to use four Go templating libraries as the basis of this article, based on usage and applicability to developers today, as several others are either precompiled or sparsely maintained. The libraries we will be comparing today are template/text
, pongo2
, liquid
, and quicktemplate
.
Let’s get started!
template/text
Go’s standard library provides a fully-featured and powerful templating engine. It is very performant and well-optimized.
Performance is the most important aspect of any computer program. Go’s standard is widely used in production, and is understandably a popular option for developers.
if your use case requires templates to change frequently, or you need to load templates on every request, then parsing quickly becomes a very important benchmark. The text
package provides various parsing methods, like parsing from a string or file, or using patterns to read files from a filesystem.
// template has loop, if statement function call etc.
BenchmarkGoParsing-10 32030 37202 ns/op
You can parse the template on startup and cache it to improve performance. If your template changes frequently, then you can use a cron, which updates at a fixed interval of time in the background.
A string substitution is the most basic function of any templating engine, and text
is super fast in string substitution.
// template -> This is a simple string {{ .Name }} with value as string
BenchmarkGoStd-10 4185343 288.8 ns/op
String substitution is faster when using struct as data context — using map might be a bit slow, since it also increases the time taken for key lookup.
// template -> This is a simple string {{ index . "name" }} with value as string
BenchmarkGoStdMapWithIndex-10 1333495 852.7 ns/op
As you can see using map is costly. Avoiding index
function allow to optimise above template.
// template -> This is a simple string {{ .Name }} with value as string`
BenchmarkGoStdMapWith-10 3563234 338.5 ns/op
.
also allows us to perform a key lookup on map.
Conditionals are a vital operation for template engines; inefficient conditional operations result in bad performance. The text
package provides support for if
operations, with support for and
and or
operations. Both of these take multiple arguments where each argument is a boolean type. text
provides reasonable performance for conditionals.
// template -> This is the file {{if and .First (eq .Second "value") (ne .Third "value")}}Got{{end}}. Git do
BenchmarkGoIfString-10 506409 2323 ns/op
The performance demonstrated above can be improved by avoiding eq
and ne
calls. Every function in template
is called via reflection, causing a performance hit here.
If eq .Second
"value"
was in the code, then the compiler would be able to optimize it since the concrete type will be known at the compiler type. If all eq
and ne
calls are replaced by a simple boolean, speeds will be faster.
// template -> This is the file {{if and .First .Four .Five}}Got{{end}}. Git do
BenchmarkGoIf-10 987169 1186 ns/op
(Note: During benchmarking, string comparisons were also included in time computations)
Loops are also provided by the text
package. range
is used to iterate over slices, arrays, and maps. Performance for loops depends on the operations contained in the loop body.
// template ->`{{range .Six}}{{.}},{{end}}`
BenchmarkGoLoop-10 1283661 919.8 ns/op
Similarly, iterating over map is also performant:
// template -> {{range $key, $value:=.Seven}}{{$key}},{{$value}}:{{end}}
BenchmarkGoLoopMap-10 566796 2003 ns/op
Function calls instead allow developers to change and transform inputs inside a template. But, this typically incurs a cost, since the function calls happen via reflection. Using functions has a high overhead cost — if possible, avoid function calls.
// template -> This is a simple string {{ noop }} with value as string
BenchmarkGoStdCallFunc-10 2611596 472.3 ns/op
Nested templates allow users to share code and avoid repeating code unnecessarily. text
provides support for calling nested templates.
// template -> {{define "noop"}} This is {{.Second}} and {{.Third}} {{end}}
//
//{{template "noop" .}}
BenchmarkGoNested-10 1868461 630.2 ns/op
It’s worth noting that text
in particular has excellent community support, with libraries like sprig
providing a wide range of functions that can be used inside a template. Support for syntax highlighting and validation built-in comes as standard with Go’s standard library templating package.
pongo2
pongo2
is a community-built template engine with syntax inspired by Django-syntax. It is built by the community for Go. It is very popular today, with more than 2K stars on GitHub.
pongo2
is well-optimized for function calls within templates. It is a full template engine with loops, conditionals, and nested templates.
pongo2
supports parsing templates from string, byte, files, and cache. We can see, performance-wise, it is a bit faster than Go’s standard library’s template package:
// template has loop, if, nested template and function call
BenchmarkPongoParsing-10 38670 29153 ns/op
String substitution is a bit slower in pongo2
when compared with the text
package. It only supports map for data context, resulting in extra lookup operation time, which is reflected in the following benchmark.
// template -> This is a simple string {{ name }} with value as string
BenchmarkPongo2-10 1815843 654.2 ns/op
pongo2
has more developer-friendly syntax for if/else. It is closer to how if/else is written in programming languages. if/else
is more costly from a performance standpoint in pongo2 than the text
package.
// template -> Name is {% if First && Four && Five %}got{% endif %}. Go
BenchmarkPongo2String2If-10 709471 1528 ns/op
Loops in pongo2
are a bit slower than text
package:
// template -> {% for value in Six %} {{ value }}, {% endfor %}
BenchmarkPongo2String2Loop-10 650672 1796 ns/op
Loops on map are also slower:
// template -> {% for key,value in Seven %} {{key}},{{ value }}, {% endfor %}
BenchmarkPongo2String2LoopMap-10 359858 3182 ns/op
Function calls are faster in pongo2
than the text
package, since it has a function signature known at compile time, and the function doesn’t need to go through a reflection call, making it faster.
// template -> This is a simple string {{ noop }} with value as string
BenchmarkPongo2StdCallFunc-10 4775058 261.9 ns/op
pongo2
macros are provided for nested template performance, which are slower than the text
package.
// template -> {% macro noop(first, second) %}
This is {{first}} and {{second}}
{% endmacro %}
{{noop("anshul","goyal")}}
BenchmarkPongo2String2Nested-10 657597 1665 ns/op
liquid
liquid
is a community-built implementation of Shopify’s template language. It provides a fully featured templating library.
liquid
is quite performant from my research. It is a full template engine with loops, conditionals, and nested templates.
liquid
supports parsing templates from string, byte, and files. In terms of performance, it is a bit slower than Go’s standard library template package and pongo2
.
// template has loop, if, nested template and function call
BenchmarkLiquidParsing-10 29710 40114 ns/op
String substitution performance is comparable with pongo2
, while liquid
is a bit slower than the text
package. It only supports map for data context, resulting in extra lookup operation time, shown in the following benchmark.
// template -> This is a simple string {{ name }} with value as string
BenchmarkLiquidString-10 1815843 676.0 ns/op
liquid
has very developer-friendly syntax for if/else. It is closer to how if/else is written in other established programming languages. if/else
is less performant in liquid than the text
package, but faster than pongo2
.
// template -> This is the file {%if First and Four and Five %}Got{%endif%}. Git do
BenchmarkLiquidIf-10 709471 953.3 ns/op
Loops in liquid
are also slower than when using the text
package. Loops are slower in liquid
even when compared with pongo2
:
// template -> {%for value in Six %}{{value}},{%endfor%}
BenchmarkLiquidLoop-10 650672 3067 ns/op
Function calls are faster in pongo2
than liquid
, since it has a function signature pulled at compile time and the function doesn’t need to go through a reflection call, making it faster.
// template -> This is a simple string {{ noop }} with value as string
BenchmarkPongo2StdCallFunc-10 4775058 359.0 ns/op
quicktemplate
quicktemplate
is a precompiled template; it converts templates into Go code. It doesn’t allow developers to change code at runtime. quicktemplate
is very fast as it is doesn’t perform any reflection and everything is run through a compiler optimizer.
If your use case doesn’t need frequent updates, then quicktemplate
might be a very good choice for you. Benchmarks for quicktemplate
in comparison to liquid
and fasttemplate
(for string substitutions) are shown below:
Feature | liquid | fasttemplate | quicktemplate |
---|---|---|---|
Parsing | 40114 ns/op | 188.8 ns/op | N/A |
If statements | 953.3 ns/op | N/A | 87.47 ns/op |
if statements with strings | 1144 ns/op | N/A | 99.18 ns/op |
loops | 3067 ns/op | N/A | 268.8 ns/op |
functions | 359.0 ns/op | N/A | 191.7 ns/op |
nested templates | N/A | N/A | 191.7 ns/op |
String Substitution | 676.0 ns/op | 75.21 ns/op | 105.9 ns/op |
quicktemplate
is a pre-compiled template engine (i.e. a template is converted to Go code). The Go code is then optimized by the compiler, resulting in very fast execution. It also avoids reflection, resulting in huge performance gains.
(Note: quicktemplate
provides fast template execution, but at the cost of no runtime updates of the template. fasttemplate
only supports string substitution)
pongo2
and text
both have their own pros and cons. pongo2
offers a bit more developer-friendly syntax than text
, but text
provides better performance overall.
Which templating library you use all depends on which suits your needs better for a particular project. text
comes pre-bundled with Go’s installation, making it naturally a very popular choice, while options like quicktemplate
can also be a good choice if your templates don’t need to be changed frequently, and others like pongo2
are easier to use if you don’t much enjoy using Go’s standard library. If you only require string substitution, then fasttemplate
is also a great choice from a performance perspective.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.