在Go模板的range循环中,循环外声明的变量在每次迭代时会被重置吗?

huangapple go评论73阅读模式
英文:

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

问题

我正在尝试在Go模板的range循环中使用在外部声明的变量来判断前一篇帖子是否与当前帖子在同一天。这里是一个简化的示例。

假设.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的值在每次迭代中被重置,并且也许提供一种实现我在这里尝试做的事情的方法吗?

英文:

I'm trying to use a variable declared outside a Go template range loop to see if the previous post occurred on the same day as the current post. Here's a simplified example.

Where .Posts is an array of post structs that each have a .Content and a .Date.

{{ $prevDate := &quot;&quot; }}
{{ range $post := .Posts }}
    {{ if ne $prevDate $post.Date }}
        &lt;div class=&quot;post-date&quot;&gt;Posts dated: {{ $post.Date }}&lt;/div&gt;
    {{ end }}
    &lt;div class=&quot;post-content&quot;&gt;{{ $post.Content }}&lt;/div&gt;
    {{ $prevDate := $post.Date }}
{{ end }}

The problem is that $prevDate seems to be reset to &quot;&quot; at the start of each iteration of the loop.

Can anyone help me understand why the value of $prevDate is reset on each iteration and perhaps suggest a way to accomplish what I'm trying to do here?

答案1

得分: 13

注意:Go 1.11将支持通过赋值修改模板变量。以下是有效的代码:

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

原始答案早于Go 1.11:


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

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

你可以使用以下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
}

// 注册函数:
var yourTempl = template.Must(template.New("").
    Funcs(map[string]interface{}{"PrevDate": PrevDate}).
    Parse(yourStringTemplate))

然后在模板中可以这样调用:

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

解决方案2:使用Posts的方法

这个解决方案类似,但更简单:为你的Posts类型添加一个方法,你可以直接调用它,无需注册函数。

例如:

type Post struct {
    // 你的Post类型
    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}}
英文:

Note: Go 1.11 will support modifying template variables via assignment. This will be valid code:

{{ $v := &quot;init&quot; }}
{{ if true }}
  {{ $v = &quot;changed&quot; }}
{{ end }}
v: {{ $v }} {{/* &quot;changed&quot; */}}

Original answer pre-dating Go 1.11 follows:


Variables are not reset. Basically what happens is that you redeclare the $prevDate variable inside the loop. But it is only in scope after the redeclaration and before the closing {{end}} tag of the {{range}}. So when the next iteraiton of the loop comes, you only see the "outer" variable which you haven't changed (because you created a new).

You can't change the values of the template variables you create.

What you can do is for example use the following range form:

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

And...

Solution #1: with a registered Function

And you can register a function for the template (see template.Funcs()) to which you can pass the $index and it would return the date field of the previous element (at $index -1).

It would look something like this:

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

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

And from your template you can call it like:

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

Solution #2: with a Method of Posts

This solution is analog but is even simpler: add a method to your Posts and you can call it directly. No need to register a function.

For example:

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

type Posts []Post

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

And from your template you can call it like:

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

答案2

得分: 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 }{}} // 初始值确保第一次更改被触发。
}

并使用模板函数将其连接到模板中:

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示例

如果帖子的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 }}

这仅在模板包保证值被视为true时才起作用。

这种解决方案不需要在每次使用时解析模板(与其他答案中的解决方案#1不同),并且适用于任意切片类型(与其他答案中的两种解决方案不同)。

英文:

Go templates are not designed to support complex logic. There's the Go programming language for that. Templates have limitations as a consequence of this philosophy. One limitation is that template variables cannot be changed.

One way to handle this limitation is to structure the data in Go to match the structure of output. Create a type to hold posts for a date and render a slice of these types. The template simply ranges through PostsForDate and Posts.

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

var Dates []PostsForDate

{{range .Dates}}
    &lt;div class=&quot;post-date&quot;&gt;Posts dated: {{.Date}}&lt;/div&gt;
    {{range .Posts}}
       &lt;div class=&quot;post-content&quot;&gt;{{.Content}}&lt;/div&gt;
    {{end}}
{{end}}

A simpler option (that goes against the design philosophy to some degree) is to create a type in Go to record a current value and report changes to that value.

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 &amp;change{&amp;struct{ int }{}} // initial value ensures that first change is fired.
}

and hook it into a template using a template function:

t := template.Must(template.New(&quot;&quot;).Funcs(template.FuncMap{&quot;change&quot;: newChange}).Parse(` some template `))

Use it in a template like this:

{{ $i := change }}
{{ range $post := .Posts }}
    {{ $i.Change $post.Date }}
        &lt;div class=&quot;post-date&quot;&gt;Posts dated: {{ $post.Date }}&lt;/div&gt;
    {{ end }}
    &lt;div class=&quot;post-content&quot;&gt;{{ $post.Content }}&lt;/div&gt;
{{ end }}

playground example

If the post Date field is a time.Time and the posts have different times within a day, then the above does not work as desired. A workaround for this is to check for changes in the rendered date (for example $post.Date.Format &quot;2006-01-02&quot;). Add the following method to simplify this:

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

Use it like this:

{{ $i := change }}
{{ range $post := .Posts }}
    {{with $i.ChangedValue ($post.Date.Format &quot;2006-01-02&quot;)}}
        &lt;div class=&quot;post-date&quot;&gt;Posts dated: {{.}}&lt;/div&gt;
    {{ end }}
    &lt;div class=&quot;post-content&quot;&gt;{{ $post.Content }}&lt;/div&gt;
{{ end }}

This only works when the values are guaranteed to be considered true by the template package.

This solution does not require parsing the template on every use (as in solution #1 in the other answer) and it applies to arbitrary slice types (unlike both solutions in the other answer).

huangapple
  • 本文由 发表于 2015年2月23日 20:48:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/28674199.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定