Visitar URL original
proposal: text/template, html/template: add MethodResolver and ExecuteWithResolver · Issue #82062 · golang/go · GitHub
Skip to content

proposal: text/template, html/template: add MethodResolver and ExecuteWithResolver #82062

Open

Description

@dims

Proposal Details

Executing a template makes reflect.Value.MethodByName reachable: evalField uses it to look up names such as .Name. The linker then keeps exported methods that would otherwise be unused (#72895, #62024). In the Kubernetes prototype, disabling method lookup reduces kubectl's binary size by 29% and kubeadm's by 48%. gRPC servers can also reach this lookup through x/net/trace.

I propose a second entry point that lets the caller provide method lookup:

package template // text/template; html/template mirrors it with a MethodResolver alias

// A MethodResolver resolves the name in a reference such as .Name to a
// function to call.
type MethodResolver interface {
	// Resolve returns the function to call for name on receiver, or nil to
	// fall through to struct fields and map keys. The function is called
	// with the reference's arguments; a method value such as
	// receiver.Name is the usual result. The receiver is the value being
	// evaluated, or a pointer to it when it is addressable.
	Resolve(receiver any, name string) any
}

// ExecuteWithResolver is like Execute but resolves methods with resolver.
// A nil resolver disables method lookup.
func (t *Template) ExecuteWithResolver(wr io.Writer, data any, resolver MethodResolver) error

Semantics:

  • Execute is unchanged and keeps resolving methods by reflection.
  • A nil resolver resolves names to struct fields and map keys only. Missing fields and map keys follow the same error and missingkey rules as Execute. This is the mode requested for ExecuteLite in proposal: text/template,html/template: Template.ExecuteLite skip resolution to methods to allow DCE #72895.
  • A non-nil resolver can select methods by receiver type or return other functions. The returned function receives the template's arguments. Returning a non-function value other than nil or panicking in Resolve causes an execution error.
  • Nested {{template}} executions use the same resolver.
  • Template functions and the String and Error methods used when printing values still run; this is not a sandbox.
  • When every template execution in a program uses a nil or non-reflective resolver, template execution does not make dynamic method lookup reachable. The linker can then drop methods that nothing else references.

I also prototyped a build tag, a nil-only execution method, and a FuncMap of receiver-first functions. A build tag applies to the whole binary and leaves the choice to whoever builds it. A nil-only method disables lookup but cannot provide custom method resolution. A FuncMap maps names to functions; dispatch by receiver type belongs inside each function.

MethodResolver receives both the receiver and the name. It needs no reflect types in the API, and callers that only need fields and map keys can pass nil.

Prototype: master...dims:go:template-method-resolver, with tests, five cmd/link dead-code fixtures, api/next and release notes; the package tests, the linker fixtures and the API check pass. With every Kubernetes call site passing nil, kubectl shrinks from 55,730,336 to 39,329,952 bytes and kubeadm from 69,464,224 to 35,799,200 bytes. There are zero reflect-method edges; help output and 91 template cases are byte-identical to the unmodified Kubernetes build.

Related compiler fix: #82047 and CL 846145. A method expression for reflect.Value.MethodByName was not marked when used as a value, and the linker pruned the methods it looked up.

Open question: the name. The prototype uses ExecuteWithResolver and MethodResolver.

Activity

  1. added this to the Proposal milestone on Oct 7, 2026
  2. gopherbot commented on Oct 7, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/846625 mentions this issue: text/template, html/template: add MethodResolver and ExecuteWithResolver

  3. AlexanderYastrebov commented on Oct 7, 2026

    @AlexanderYastrebov
    Contributor

    What do you think about making it generic like:

    package template
    
    // Evaluator returns named value from data.
    type Evaluator interface {
    	Eval(data any, name string) (any, bool)
    }
    
    // FieldEvaluator returns value of the struct field.
    var FieldEvaluator Evaluator
    
    // KeyEvaluator returns value of the map key.
    var KeyEvaluator Evaluator
    
    // MethodEvaluator calls named method and returns its value.
    var MethodEvaluator Evaluator
    
    // MultiEvaluator returns an Evaluator that's the logical concatenation of the provided input evaluators.
    func MultiEvaluator(evals ...Evaluator) Evaluator {...}
    
    // ExecuteWithEvaluator is like Execute but uses evaluator to get values from data.
    func (t *Template) ExecuteWithEvaluator(wr io.Writer, data any, eval Evaluator) error
    
    func ExampleTemplate_ExecuteWithEvaluator() {
      type foo struct {
        Bar string
      }
      data := map[string]foo{
        "Foo": foo{"baz"},
      }
    
      t := template.Must(template.New("t").Parse("{{.Foo.Bar}}"))
      t.ExecuteWithEvaluator(io.Discard, data, template.MultiEvaluator(
        template.FieldEvaluator, template.KeyEvaluator, /*template.MethodEvaluator,*/
      ))
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions