Search code examples
stringdictionarygogo-templatesgo-html-template

How does text/template determine the "default textual representation" of a map?


Per the documentation of the text/template package in the Go standard library, (html/template would be the same here as far as I know) simply using the pipeline operator will spit out a "default textual representation" of whatever that is:

{{pipeline}}

The default textual representation of the value of the pipeline is copied to the output.

In the case of a map, you get a nice printed-out format with key names and everything...incidentally, this is valid JavaScript, so it makes it easy to pass whole structs into your JS code if you want.

My question is, how is this textual representation determined, and more specifically can I hook into it? I thought perhaps it would check to see if the pipeline was a fmt.Stringer and I could give my map subtype a String() string method, but that doesn't seem to be the case. I'm hunting through the text/template code but I seem to be missing how it does this.

How does text/template determine "default textual representation"?


Solution

  • The default textual representation is determined by how the fmt package prints the value. So you were barking up the right tree.

    See this example:

    t := template.Must(template.New("").Parse("{{.}}"))
    m := map[string]interface{}{"a": "abc", "b": 2}
    t.Execute(os.Stdout, m)
    

    It outputs:

    map[a:abc b:2]
    

    Now if we use a custom map type with a String() method:

    type MyMap map[string]interface{}
    
    func (m MyMap) String() string { return "custom" }
    
    mm := MyMap{"a": "abc", "b": 2}
    t.Execute(os.Stdout, mm)
    

    Output is:

    custom
    

    Try these (and below examples) on the Go Playground.

    What to look out for?

    Note that MyMap.String() has a value receiver (not a pointer). And I pass a value of MyMap, so it works. If you change the receiver type to pointer to MyMap, it won't work. And it is because then only a value of type *MyMap will have a String() method, but not a value of MyMap.

    If the String() method has a pointer receiver, you have to pass &mm (a value of type *MyMap) if you want your custom representation to work.

    Also note that in case of html/template, the template engine does contextual escaping, so the result of the fmt package may further be escaped.

    For example if your custom String() method would return something "unsafe":

    func (m MyMap2) String() string { return "<html>" }
    

    Trying to insert it:

    mm2 := MyMap2{"a": "abc", "b": 2}
    t.Execute(os.Stdout, mm2)
    

    Gets escaped:

    &lt;html&gt;
    

    Implementation

    This is where it is implemented in the text/template package: text/template/exec.go, unexported function state.PrintValue(), currently line #848:

    _, err := fmt.Fprint(s.wr, iface)
    

    If you're using the html/template package, it's implemented in html/template/content.go, unexported function stringify(), currently line #135:

    return fmt.Sprint(args...), contentTypePlain
    

    Further options

    Also note that if the value implements error, the Error() method will be called and it takes precedence over String():

    type MyMap map[string]interface{}
    
    func (m MyMap) Error() string { return "custom-error" }
    
    func (m MyMap) String() string { return "custom" }
    
    t := template.Must(template.New("").Parse("{{.}}"))
    mm := MyMap{"a": "abc", "b": 2}
    t.Execute(os.Stdout, mm)
    

    Will output:

    custom-error
    

    Instead of custom. Try it on the Go Playground.