Compare two variables inside Go template

Tobias Meyer picture Tobias Meyer · Jun 24, 2017 · Viewed 7.6k times · Source

In the data I pass to my template I have the two variables Type and Res.Type I want to compare to preselect an option for my select field.

To illustrate my problem I have created this simplified version:

package main

import (
    "bufio"
    "bytes"
    "html/template"
    "log"
)

type Result struct{ Type string }

func main() {
    types := map[string]string{
        "FindAllString":      "FindAllString",
        "FindString":         "FindString",
        "FindStringSubmatch": "FindStringSubmatch",
    }
    res := &Result{Type: "findAllString"}

    templateString := `
    <select name="type">
        {{ range $key,$value := .Types }}
            {{ if eq $key .Res.Type }}
                <option value="{{$key}}" selected>{{$value}}</option>
            {{ else }}
                <option value="{{$key}}">{{$value}}</option>
            {{ end }}
        {{ end }}
    </select>`
    t, err := template.New("index").Parse(templateString)
    if err != nil {
        panic(err)
    }
    var b bytes.Buffer
    writer := bufio.NewWriter(&b)
    err = t.Execute(writer, struct {
        Types map[string]string
        Res   *Result
    }{types, res})
    if err != nil {
        panic(err)
    }
    writer.Flush()
    log.Println(b.String())
}

It should select the "FindAllString" option but it only generates the error

panic: template: index:4:21: executing "index" at <.Res.Type>: can't evaluate field Res in type string

goroutine 1 [running]:
panic(0x53f6e0, 0xc4200144c0)
    /usr/local/go/src/runtime/panic.go:500 +0x1a1
main.main()
    /home/tobias/ngo/src/github.com/gamingcoder/tmp/main.go:41 +0x523
exit status 2

When I just compare two normal strings it works but I want to know if there is an idomatic way to do this. I have seen that you could add a function to the template but I feel that there must be a simpler way for this.

Answer

icza picture icza · Jun 24, 2017

The problem is that the {{range}} action changes (sets) the dot (.) even if you use loop variables ($key and $value) in your case. Inside a {{range}} the dot is set to the current element.

And inside {{range}} you write:

{{ if eq $key .Res.Type }}

Since values in your loop are string values, .Res.Type is an error, because there is no Res field or method of a string value (the current element denoted by the dot .).

Use the $ sign to not refer to the loop value, but to the param passed to the template execution:

{{ if eq $key $.Res.Type }}

This will work, but won't give you the desired output, as you have a typo:

res := &Result{Type: "findAllString"}

Use capital letter in Result as your types map also contains values with capital letter:

res := &Result{Type: "FindAllString"}

With this you get the desired output (try it on the Go Playground):

2009/11/10 23:00:00 
    <select name="type">
                <option value="FindAllString" selected>FindAllString</option>
                <option value="FindString">FindString</option>
                <option value="FindStringSubmatch">FindStringSubmatch</option>
    </select>

Also note that you could simply write the loop like this:

{{range $key, $value := .Types}}
    <option value="{{$key}}"{{if eq $key $.Res.Type}} selected{{end}}>{{.}}</option>
{{end}}

Also note that for testing purposes you may simply pass os.Stdout as the writer for template execution, and you'll see the result on your console without having to create and use a buffer, e.g.:

err = t.Execute(os.Stdout, struct {
    Types map[string]string
    Res   *Result
}{types, res})

Try the simplified version on the Go Playground.

Read this answer for more insights: golang template engine pipelines