While working on my static site generator I stumbled upon the following one-man requirement:
It would be great if I can execute custom template functions
Now, while Go provides a way to define, and later use, any kind of function
in templates via template.FuncMap
and template.Funcs
, the deal here was
to be able to define such functions on the fly, and link them to the already
existing main executable (the static site generator).
So, what we need is a way to create our own library of template functions, which can be linked to the main executable on demand, at runtime. This way we can always update the library without having to recompile the main executable again. This sounds a lot quite similar to the concept of dynamic linking. Dynamic linking, in a nutshell, means to bind a (shared) library into a running process. The main reason this technique exists is to save on memory usage: multiple processes using the same shared library can point to a single copy of such library in physical memory. For our particular use case, though, the main advantage is to be able to change the shared library without having to recompile the main executable.
By googling “go dynamic linking” I ended up in a doc called Go execution modes.
The doc (dated from 2014, and updated in 2016) describes future (now current)
directions for Go support for shared libraries and related ways of building
Go programs. Among all the available build modes described in the doc, the build
mode plugin
seems to be the most suitable one for our original requirement.
Let’s say we want our h1
s to be rendered with a fancy capital letter (like in the
title of this blog post). The final result should look as follows:
<h1><span class="capital-letter">G</span>olang plugins</h1>
And the template that generates the previous HTML should be as easy as:
<h1>{{ .myHeader | fancyTitle }}</h1>
The implementation of fancyTitle
should be straightforward:
File plugins/plugin.go
package main // Plugins must live within the main package
import (
"fmt"
"html/template"
)
var MyFuncs = template.FuncMap {
"fancyTitle": func(s string) template.HTML {
if len(s) < 2 {
return template.HTML(s)
}
return template.HTML(
fmt.Sprintf(`<span class="capital-letter">%s</span>%s`, s[0:1], s[1:]),
)
},
}
We put our function within a template.FuncMap
because that’s what template.Funcs
expects
as parameter. The main executable can then load this plugin as follows.
File main.go
package main
import (
"fmt"
"html/template"
"os"
"os/exec"
"plugin"
)
const PluginFilepath = "plugins/plugin.so"
func main() {
var myFuncs template.FuncMap
myFuncs = loadFuncs()
t, err := template.New("mypage").Funcs(template.FuncMap(myFuncs)).ParseFiles("hello.html")
if err != nil {
panic(err)
}
err = t.ExecuteTemplate(os.Stdout, "hello.html", nil)
if err != nil {
panic(err)
}
}
// LoadFuncs loads the custom template functions from a hardcoded location
func loadFuncs() template.FuncMap {
// Hardcoded location of plugins
cmd := exec.Command("go", "build", "-buildmode=plugin", "-o", PluginFilepath, "plugins/plugin.go")
err := cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Could not build plugin: %s", err)
os.Exit(1)
}
plugin, err := plugin.Open(PluginFilepath)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not load plugin: %s", err)
os.Exit(1)
}
myFuncsSymbol, err := plugin.Lookup("MyFuncs")
if err != nil {
fmt.Fprintf(os.Stderr, "Could not find the symbol MyFuncs: %s", err)
os.Exit(1)
}
var myFuncs *template.FuncMap
myFuncs, ok := myFuncsSymbol.(*template.FuncMap)
if !ok {
fmt.Fprintf(os.Stderr, "MyFuncs must be of type template.FuncMap")
os.Exit(1)
}
return *myFuncs
}
File hello.html
<h1>{{ Golang plugins | fancyTitle }}</h1>
Explanation of the function loadFuncs
:
plugins/plugin.go
into the file plugins/plugin.so
plugins/plugin.so
MyFuncs
in the pluginMyFuncs
is *template.FuncMap
1, and if so we
return itIn this toy example, there are some assumptions about how the plugin looks like. The relative location of the plugin should be known in advance, as well as the name and type of the exported symbol. In a more real-world scenario, things could be a bit more flexible though.
There are multiple reasons why Go plugins do not get much love in the Go community. To name a few:
both the main executable and the plugin(s) must be built using the exact same Go compiler version
if the plugin(s) and the main executable use third-party packages, these versions must match exactly as well
plugins do not work if the main executable was statically built (CGO_ENABLED=0
),
[1]. When using Lookup
to search for a global variable, you get a pointer
(see this GitHub issue).