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

In a Go template range loop, are variables declared outside the loop reset on each iteration?

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

其中 .Posts 是一个 post 结构数组,每个结构都有一个 .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 的值在每次迭代时都被重置,并且可能会建议一种方法来完成我在这里尝试做的事情吗?

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

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

Go 1.11 之前的原始答案如下:


变量未重置。基本上发生的事情是您在循环内重新声明 $prevDate 变量。但它仅在重新声明之后和 {{range}} 的结束 {{end}} 标记之前的范围内。因此,当循环的下一次迭代到来时,您只会看到未更改的 "outer" 变量(因为您创建了一个新变量)。

您不能更改您创建的模板变量的值。

例如,您可以使用以下 range 形式:

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

还有...

解决方案 #1:使用已注册的函数

并且您可以为模板注册一个函数(参见 template.Funcs()),您可以将 $index 传递给该函数,它将 return 前一个元素的日期字段(位于$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}}

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

处理此限制的一种方法是在 Go 中构建数据以匹配输出结构。创建一个类型来保存日期的 posts 并呈现这些类型的一部分。该模板仅包含 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

如果 post Date 字段是 time.Time 并且 post 一天内有不同的时间,则上述方法无法正常工作。解决方法是检查呈现日期的变化(例如 $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 所示),它适用于任意切片类型(与另一个答案中的两种解决方案不同)。