Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variables inside templates in golang

What is the namespace of variables inside html/text templates? I thought that a variable $x can change value inside a template, but this example shows me that I cannot.

I failed when I tried to group tournaments according year - something like this (http://play.golang.org/p/EX1Aut_ULD):

package main

import (
    "fmt"
    "os"
    "text/template"
    "time"
)

func main() {
    tournaments := []struct {
        Place string
        Date  time.Time
    }{
        // for clarity - date is sorted, we don't need sort it again
        {"Town1", time.Date(2015, time.November, 10, 23, 0, 0, 0, time.Local)},
        {"Town2", time.Date(2015, time.October, 10, 23, 0, 0, 0, time.Local)},
        {"Town3", time.Date(2014, time.November, 10, 23, 0, 0, 0, time.Local)},
    }
    t, err := template.New("").Parse(`
{{$prev_year:=0}}
{{range .}}
    {{with .Date}}
        {{$year:=.Year}}
                    {{if ne $year $prev_year}}
                        Actions in year {{$year}}:
                {{$prev_year:=$year}}
            {{end}}
    {{end}}

        {{.Place}}, {{.Date}}
    {{end}}

    `)
    if err != nil {
        panic(err)
    }
    err = t.Execute(os.Stdout, tournaments)
    if err != nil {
        fmt.Println("executing template:", err)
    }
}
like image 624
lofcek Avatar asked Oct 08 '15 22:10

lofcek


3 Answers

In go1.11 text/template and hence html/template became able to set the value of existing variables, which means that the original code can be made to work with one very small modification.

Change

{{$prev_year:=$year}}

To

{{$prev_year = $year}}

Playground

like image 59
Nick Craig-Wood Avatar answered Oct 19 '22 23:10

Nick Craig-Wood


Edit: See https://stackoverflow.com/a/52925780/1685538 for a more up-to-date answer.


Original answer:

https://golang.org/pkg/text/template/#hdr-Variables:

A variable's scope extends to the "end" action of the control structure ("if", "with", or "range") in which it is declared, or to the end of the template if there is no such control structure.

So the $prev_year you define with {{$prev_year:=$year}} only lives until.. the next line ({{end}}).

It seems there is no way of going around that.

The "right" way to do this is to take that logic out of your template, and do the grouping in your Go code.

Here is a working example : https://play.golang.org/p/DZoSXo9WQR

package main

import (
    "fmt"
    "os"
    "text/template"
    "time"
)

type Tournament struct {
    Place string
    Date  time.Time
}

type TournamentGroup struct {
    Year        int
    Tournaments []Tournament
}

func groupTournamentsByYear(tournaments []Tournament) []TournamentGroup {
    if len(tournaments) == 0 {
        return nil
    }

    result := []TournamentGroup{
        {
            Year:        tournaments[0].Date.Year(),
            Tournaments: make([]Tournament, 0, 1),
        },
    }

    i := 0
    for _, tournament := range tournaments {
        year := tournament.Date.Year()
        if result[i].Year == year {
            // Add to existing group
            result[i].Tournaments = append(result[i].Tournaments, tournament)
        } else {
            // New group
            result = append(result, TournamentGroup{
                Year: year,
                Tournaments: []Tournament{
                    tournament,
                },
            })
            i++
        }
    }

    return result
}

func main() {
    tournaments := []Tournament{
        // for clarity - date is sorted, we don't need sort it again
        {"Town1", time.Date(2015, time.November, 10, 23, 0, 0, 0, time.Local)},
        {"Town2", time.Date(2015, time.October, 10, 23, 0, 0, 0, time.Local)},
        {"Town3", time.Date(2014, time.November, 10, 23, 0, 0, 0, time.Local)},
    }

    t, err := template.New("").Parse(`
{{$prev_year:=0}}
{{range .}}
    Actions in year {{.Year}}:
    {{range .Tournaments}}

            {{.Place}}, {{.Date}}
    {{end}}
    {{end}}

    `)
    if err != nil {
        panic(err)
    }
    err = t.Execute(os.Stdout, groupTournamentsByYear(tournaments))
    if err != nil {
        fmt.Println("executing template:", err)
    }
}
like image 17
HectorJ Avatar answered Oct 19 '22 22:10

HectorJ


As mentioned by this answer, the scope of that variable "re-assignment" ends with the {{end}} block. Therefore using standard variables only there's no way around the problem and it should be solved inside the Go program executing the template.

In some frameworks however this is not that easy (e.g. protoc-gen-gotemplate).

The Sprig library adds additional functionality to the standard template language. One of them are mutable maps that can be used in the following way:

// init the dictionary (you can init it without initial key/values as well)
{{$myVar := dict "key" "value"}}

// getting the "key" from the dictionary (returns array) and then fetching the first element from that array
{{pluck "key" $myVar | first}}

// conditional update block
{{if eq "some" "some"}}
     // the $_ seems necessary because Go template functions need to return something
     {{$_ := set $myVar "key" "newValue"}}
{{end}}

// print out the updated value
{{pluck "key" $myVar | first}}

This little example prints out:

value
newValue

A pragmatic approach would be to use a single dictionary for all mutable variables and store them under their corresponding variable name as key.

Reference:

  • http://masterminds.github.io/sprig/dicts.html
  • https://github.com/Masterminds/sprig
like image 6
Peter Ilfrich Avatar answered Oct 19 '22 21:10

Peter Ilfrich