Skip to content

proposal: text/template: revise execution model #36462

Open
@bep

Description

@bep

Go's template packages (text/template, html/template) do in general work really well. But there are some issues that are hard/impossible to work around.

In the current implementation, a Template struct is mirrored in both packages, and while there is some separation of parsing and execution to allow for concurrent execution, it is very much intermingled.

This proposal suggests adding a new "template executer" which would:

  1. Prepare and execute the template
  2. Store and resolve any template functions
  3. Provide some user-defined hook interface to allow for a custom evaluation of method/func/map lookups etc.

The above may look like different things, candidates for different proposals. But these issues are connected and I'm convinced that it helps to look at them as a whole, even if this proposal is only partly accepted.

The benefits of the above would be:

  1. Stateful functions. One common example would be a i18n translation func. You could make it a method ({{ .I18n "hello" }}) or make it take the language as the first argument ({{ i18n .Language "hello" }}), neither very practical. The current workaround is to clone the template set for each language, which is both resource wasteful and hard to get right in the non-trivial setups. I assume that these functions are added early to get parse-time signature validation, a good thing, but I don't see a reason why not to use another set for execution, making the function lookup lock-free a great performance bonus.
  2. Some examples include case insensitive map lookups, adding some execution context to methods that support it (proposal: text/template, html/template: add ExecuteContext methods #31107), allow range of funcs that return map/slice (text/template: Methods vs funcs discrepancy #20503), allow a custom variant of the built-in and rather opinionated version of IsTrue (text/template: allow callers to override IsTrue #28391)

Doing this also removes some performance bottlenecks. A relevant benchmark in Hugo before/after we made these changes:

name                            old time/op    new time/op    delta
SiteNew/Many_HTML_templates-16    55.6ms ± 2%    42.9ms ± 1%  -22.81%  (p=0.008 n=5+5)

name                            old alloc/op   new alloc/op   delta
SiteNew/Many_HTML_templates-16    22.5MB ± 0%    17.6MB ± 0%  -21.99%  (p=0.008 n=5+5)

name                            old allocs/op  new allocs/op  delta
SiteNew/Many_HTML_templates-16      341k ± 0%      247k ± 0%  -27.48%  (p=0.008 n=5+5)

Implementation outline

In the Hugo project we have worked around the problems outlined above, some workaround less pretty than others. But recently we stumbled into a hurdle we didn't find a way around, so we closed our eyes and created a scripted fork of the text/template and html/template packages. We will pull in upstream fixes, and the ultimate goal is for that fork to go away.

But this means that there is a working implementation of this proposal. The implementation is a little bit coloured by trying to do the least amount of changes to the existing code, but I think it outlines a possible API. And it is backwards compatible.

// Executer executes a given template.
type Executer interface {
	Execute(p Preparer, wr io.Writer, data interface{}) error
}
// Preparer prepares the template before execution.
type Preparer interface {
	Prepare() (*Template, error)
}
// ExecHelper allows some custom eval hooks.
type ExecHelper interface {
	GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
	GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value)
	GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool)
	IsTrue(val reflect.Value) bool
}

Hugo's fork can be found here https://github.com/gohugoio/hugo/tree/master/tpl/internal/go_templates (all the patches are placed in hugo_*.go files). Changes are marked with the comment Added for Hugo.. The script that maintains the fork can be found at https://github.com/gohugoio/hugo/tree/master/scripts/fork_go_templates.

Some related Go issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    Proposalv2An incompatible library change

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions