Repository: go
Issue: proposal: text/template: revise execution model
Labels: v2,Proposal

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:

2. 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.
3. Some examples include case insensitive map lookups, adding some execution context to methods that support it (https://github.com/golang/go/issues/31107), allow range of funcs that return map/slice (https://github.com/golang/go/issues/20503), allow a custom variant of the built-in and rather opinionated version of `IsTrue` (https://github.com/golang/go/issues/28391)

Doing this also removes some performance bottlenecks. A relevant benchmark in Hugo before/after we made these changes:

```bash
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](https://github.com/gohugoio/hugo) 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.

```go
// Executer executes a given template.
type Executer interface {
	Execute(p Preparer, wr io.Writer, data interface{}) error
}
```

```go
// Preparer prepares the template before execution.
type Preparer interface {
	Prepare() (*Template, error)
}
```

```go
// 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

* Add ExecuteContext methods https://github.com/golang/go/issues/31107
* Document ability to modify FuncMap after template parse by calling Funcs again https://github.com/golang/go/issues/34680
* Allow callers to override IsTrue https://github.com/golang/go/issues/28391
* Methods vs funcs discrepancy  https://github.com/golang/go/issues/20503
* index should return nil instead of index out of range error https://github.com/golang/go/issues/14751