在Go模板范围循环中,是在每次迭代时在循环重置之外声明的变量吗?

时间:2015-02-23 12:48:28

标签: go go-templates

我试图使用在Go模板范围循环外声明的变量来查看上一篇文章是否与当前帖子发生在同一天。这是一个简化的例子。

其中.Posts是一组帖子结构,每个结构都有.Content.Date

{{ $prevDate := "" }}
{{ range $post := .Posts }}
    {{ if ne $prevDate $post.Date }}
        <div class="post-date">Posts dated: {{ $post.Date }}</div>
    {{ end }}
    <div class="post-content">{{ $post.Content }}</div>
    {{ $prevDate := $post.Date }}
{{ end }}

问题是$prevDate似乎在循环的每次迭代开始时重置为""

任何人都可以帮助我理解为什么$prevDate的值会在每次迭代时被重置,并且可能会建议一种方法来完成我在这里尝试做的事情吗?

2 个答案:

答案 0 :(得分:8)

注意: Go 1.11 will support modifying template variables via assignment。这将是有效的代码:

{{ $v := "init" }}
{{ if true }}
  {{ $v = "changed" }}
{{ end }}
v: {{ $v }} {{/* "changed" */}}

原始答案预先约会Go 1.11如下:


不会重置变量。基本上发生的是你重新声明循环内的$prevDate变量。但它仅在重新声明后的范围内以及{{end}}的结束{{range}}标记之前。所以当循环的下一个迭代出现时,你只看到你没有改变的“外部”变量(因为你创建了一个新的)。

您无法更改您创建的模板变量的值。

您可以做的是使用以下range表单:

{{ range $index, $post := .Posts }}

和...

解决方案#1:具有注册功能

您可以为模板注册一个函数(请参阅template.Funcs()),您可以传递$index,并返回上一个元素的日期字段($index -1

它看起来像这样:

func PrevDate(i int) string {
    if i == 0 {
        return ""
    }
    return posts[i-1].Date
}

// Registering it:
var yourTempl = template.Must(template.New("").
    Funcs(map[string]interface{}{"PrevDate": PrevDate}).
    Parse(yourStringTemplate))

您可以在模板中将其命名为:

{{range $index, $post := .Posts}}
    {{$prevDate := PrevDate $index}}
{{end}}

解决方案#2:使用帖子方法

此解决方案是模拟的,但更简单:向Posts添加方法,您可以直接调用它。无需注册功能。

例如:

type Post struct {
    // Your Post type
    Date string
}

type Posts []Post

func (p *Posts) PrevDate(i int) string {
    if i == 0 {
        return ""
    }
    return (*p)[i-1].Date
}

您可以在模板中将其命名为:

{{range $index, $post := .Posts}}
    {{$prevDate := $.Posts.PrevDate $index}}
{{end}}

答案 1 :(得分:2)

Go模板不是为支持复杂逻辑而设计的。有Go编程语言。由于这种理念,模板具有局限性。一个限制是模板变量无法更改。

处理此限制的一种方法是在Go中构造数据以匹配输出结构。创建一个类型来保存日期的帖子并呈现这些类型的切片。模板的范围通过PostsForDate和Posts。

type PostsForDate struct {
    Date time.Time
    Posts []*Post
}

var Dates []PostsForDate

{{range .Dates}}
    <div class="post-date">Posts dated: {{.Date}}</div>
    {{range .Posts}}
       <div class="post-content">{{.Content}}</div>
    {{end}}
{{end}}

一个更简单的选项(在某种程度上违背了设计理念)是在Go中创建一个类型来记录当前值并报告对该值的更改。

type change struct {
    current interface{}
}

func (c *change) Changed(next interface{}) bool {
    result := c.current != next
    c.current = next
    return result
}

func newChange() *change {
    return &change{&struct{ int }{}} // initial value ensures that first change is fired.
}

并使用template function

将其挂钩到模板中
t := template.Must(template.New("").Funcs(template.FuncMap{"change": newChange}).Parse(` some template `))

在这样的模板中使用它:

{{ $i := change }}
{{ range $post := .Posts }}
    {{ $i.Change $post.Date }}
        <div class="post-date">Posts dated: {{ $post.Date }}</div>
    {{ end }}
    <div class="post-content">{{ $post.Content }}</div>
{{ end }}

playground example

如果帖子Date字段为time.Time且帖子在一天内有不同的时间,则上述内容无法按预期运行。解决方法是检查呈现日期的更改(例如$post.Date.Format "2006-01-02")。添加以下方法以简化此操作:

func (c *change) ChangedValue(next interface{}) interface{} {
    if c.current != next {
        c.current = next
        return next
    }
    return nil
}

像这样使用:

{{ $i := change }}
{{ range $post := .Posts }}
    {{with $i.ChangedValue ($post.Date.Format "2006-01-02")}}
        <div class="post-date">Posts dated: {{.}}</div>
    {{ end }}
    <div class="post-content">{{ $post.Content }}</div>
{{ end }}

仅当模板包保证值被视为真时才有效。

此解决方案不需要在每次使用时解析模板(如在另一个答案中的解决方案#1中),并且它适用于任意切片类型(与另一个答案中的两个解决方案不同)。