调用具有多个管道参数的模板

Calling a template with several pipeline parameters

提问人:Deleplace 提问时间:8/16/2013 最后编辑:ROMANIA_engineerDeleplace 更新时间:8/15/2023 访问量:43795

问:

在 Go 模板中,有时将正确的数据传递到正确的模板的方式对我来说很尴尬。使用管道参数调用模板就像调用只有一个参数的函数。

假设我有一个关于 Gophers 的 Gophers 网站。它有一个主页主模板,以及一个用于打印 Gophers 列表的实用程序模板。

http://play.golang.org/p/Jivy_WPh16

输出:

*The great GopherBook*    (logged in as Dewey)

    [Most popular]  
        >> Huey
        >> Dewey
        >> Louie

    [Most active]   
        >> Huey
        >> Louie

    [Most recent]   
        >> Louie

现在我想在子模板中添加一些上下文:在列表中以不同的方式格式化名称“Dewey”,因为它是当前登录用户的名称。但是我不能直接传递名称,因为只有一个可能的“点”参数管道!我能做些什么?

  • 显然,我可以将子模板代码复制粘贴到主模板中(我不想这样做,因为它放弃了拥有子模板的所有兴趣)。
  • 或者我可以用访问器处理某种全局变量(我也不想)。
  • 或者我可以为每个模板参数列表创建一个新的特定结构类型(不是很好)。
去模板

评论


答:

0赞 Deleplace 8/16/2013 #1

到目前为止,我发现的最好的(我不太喜欢它)是带有“通用”对容器的多路复用和解复用参数:

http://play.golang.org/p/ri3wMAubPX

type PipelineDecorator struct {
    // The actual pipeline
    Data interface{}
    // Some helper data passed as "second pipeline"
    Deco interface{}
}

func decorate(data interface{}, deco interface{}) *PipelineDecorator {
    return &PipelineDecorator{
        Data: data,
        Deco: deco,
    }
}

我经常使用这个技巧来构建我的网站,我想知道是否存在一些更惯用的方法来实现相同的目的。

78赞 tux21b 8/16/2013 #2

您可以在模板中注册一个“dict”函数,该函数可用于将多个值传递给模板调用。然后,调用本身将如下所示:

{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}

小“dict”助手的代码,包括将其注册为模板 func 在这里:

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
    "dict": func(values ...interface{}) (map[string]interface{}, error) {
        if len(values)%2 != 0 {
            return nil, errors.New("invalid dict call")
        }
        dict := make(map[string]interface{}, len(values)/2)
        for i := 0; i < len(values); i+=2 {
            key, ok := values[i].(string)
            if !ok {
                return nil, errors.New("dict keys must be strings")
            }
            dict[key] = values[i+1]
        }
        return dict, nil
    },
}).ParseGlob("templates/*.html")

评论

0赞 Deleplace 8/16/2013
这很好,谢谢。因此,它是一个多键/值多路复用器,类似于 PipelineDecorator(请参阅其他答案),但在一次调用中具有更多可能的值。
1赞 tux21b 8/16/2013
在我开始编写这段代码之前,我以为它会变得非常丑陋,但现在我自己有点喜欢它,我可能会在我的一些项目中使用它。
4赞 Deleplace 8/17/2013
我将其标记为已接受,因为恕我直言,它是流水线任意数据的“最新技术”(但仍然是关于模板化设计选择的解决方法)。这是我重新实现的整个示例: play.golang.org/p/oWdPlyWfvGdict()
0赞 tbruyelle 10/19/2017
您是否能够调用附加到传递的值的函数?我目前有问题。由于该值变为 ,似乎不再可能对附加到它的函数进行寻址。dictinterface{}
3赞 Mario Pérez Alarcón 7/12/2019
定义了以下函数:masterminds.github.io/sprig/dicts.htmlsprig
5赞 val 8/17/2013 #3

您可以在模板中定义函数,并将这些函数作为对数据定义的闭包,如下所示:

template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User);},}

然后,您可以简单地在模板中调用此函数:

{{define "sub"}}

    {{range $y := .}}>> {{if isUser $y}}!!{{$y}}!!{{else}}{{$y}}{{end}}
    {{end}}
{{end}}

操场上的这个更新版本输出非常接近当前用户:!!

*The great GopherBook*    (logged in as Dewey)

[Most popular]  

>> Huey
>> !!Dewey!!
>> Louie



[Most active]   

>> Huey
>> Louie



[Most recent]   

>> Louie

编辑

由于您可以在调用时覆盖函数,因此您实际上可以在编译模板时预填充模板函数,并使用实际闭包更新它们,如下所示:Funcs

var defaultfuncs = map[string]interface{} {
    "isUser": func(g Gopher) bool { return false;},
}

func init() {
    // Default value returns `false` (only need the correct type)
    t = template.New("home").Funcs(defaultfuncs)
    t, _ = t.Parse(subtmpl)
    t, _ = t.Parse(hometmpl)
}

func main() {
    // When actually serving, we update the closure:
    data := &HomeData{
        User:    "Dewey",
        Popular: []Gopher{"Huey", "Dewey", "Louie"},
        Active:  []Gopher{"Huey", "Louie"},
        Recent:  []Gopher{"Louie"},
    }
    t.Funcs(template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User); },})
    t.ExecuteTemplate(os.Stdout, "home", data)
}

虽然我不确定当几个 goroutines 尝试访问同一个模板时它是如何播放的......

工作示例

评论

0赞 Deleplace 8/17/2013
好的,所以在当前模板执行的范围内,我们定义了一些有趣数据的“全局访问器”。恕我直言,主要缺点是我无法重用已编译的模板,我被迫创建带有闭包的 FuncMap,然后编译,然后为每个请求执行模板。
0赞 val 8/17/2013
是的,我认为在您的情况下,tux21b 的解决方案可能更灵活。最终,你只需要以一种或另一种方式打包你所有的价值观......
0赞 Deleplace 8/17/2013
哇,很高兴知道在模板编译后实际上可以绑定新的闭包,我不知道这一点。对于潜在的竞争条件,我确实尝试过并用于强制 goroutine interleaving,并用以下方法暴露了缺陷: play.golang.org/p/b5WVlUlGjSruntime.GoSched()panic()
0赞 Deleplace 8/19/2013
+1,因为我在 Go 模板的请求上下文中窃取了您的答案
0赞 Deleplace 5/23/2014
线程安全问题可以通过在使用前克隆模板来解决。不过,这是有代价的。
0赞 zzzz 8/17/2013 #4

广告“......看起来像调用一个只有一个参数的函数。

从某种意义上说,每个函数都采用一个参数 - 多值调用记录。对于模板也是如此,“调用”记录可以是原始值,也可以是多值 {map,struct,array,slice}。模板可以从任何位置的“单个”管道参数中选择要使用的 {key,field,index}。

哎呀,在这种情况下一个就足够了。

评论

2赞 Deleplace 8/17/2013
确切地说:我关心的不是可行性,而是干净的设计。看看其他没有多个返回值的语言(C、Java):定义一个欧几里得除法产生一个商和一个余数是很相似的。想象一下,在一种语言中,函数只有一个参数,你必须在每个参数列表之前使用 4 或 5 行来声明一个新的数据结构:编码会感觉不好。
0赞 Darshan Rivka Whittle 8/19/2013 #5

有时,地图是解决此类情况的快速简便的解决方案,如其他几个答案中所述。由于您经常使用 Gophers(并且,根据您的另一个问题,您关心当前的 Gopher 是否是管理员),我认为它应该有自己的结构:

type Gopher struct {
    Name string
    IsCurrent bool
    IsAdmin bool
}

以下是 Playground 代码的更新:http://play.golang.org/p/NAyZMn9Pep

显然,手动编码具有额外深度的示例结构有点麻烦,但由于在实践中它们将是机器生成的,因此可以根据需要进行标记。IsCurrentIsAdmin

0赞 chowey 10/1/2014 #6

我处理这个问题的方法是装饰一般管道:

type HomeData struct {
    User    Gopher
    Popular []Gopher
    Active  []Gopher
    Recent  []Gopher
}

通过创建特定于上下文的管道:

type HomeDataContext struct {
    *HomeData
    I interface{}
}

分配特定于上下文的管道非常便宜。您可以通过复制指向它的指针来访问可能较大的内容:HomeData

t.ExecuteTemplate(os.Stdout, "home", &HomeDataContext{
    HomeData: data,
})

由于嵌入在 中,您的模板将直接访问它(例如,您仍然可以 do 和 not )。此外,您现在可以访问自由格式字段 ()。HomeDataHomeDataContext.Popular.HomeData.Popular.I

最后,我做了一个函数。UsingHomeDataContext

func (ctx *HomeDataContext) Using(data interface{}) *HomeDataContext {
    c := *ctx // make a copy, so we don't actually alter the original
    c.I = data
    return &c
}

这允许我保留一个状态(),但将任意值传递给子模板。HomeData

请参见 http://play.golang.org/p/8tJz2qYHbZ

评论

0赞 Deleplace 10/1/2014
装饰和嵌入是很有前途的想法,但我特别希望子模板可以访问上下文。您能否通过编辑原始示例来解决这个问题?谢谢!
0赞 chowey 10/2/2014
不幸的是,您的第一条评论是完全准确的:“使用管道参数调用模板看起来像调用只有一个参数的函数。我在保留整个“上下文”对象并在其上更改一个字段方面取得了最大的成功。
0赞 Deleplace 10/2/2014
好的,所以它看起来像 PipelineDecorator(参见其他答案)。将其作为方法而不是函数是个好主意。Using
0赞 chowey 10/3/2014
有趣的是,你是对的。唯一真正的区别是将 嵌入其中,而不是将其作为命名字段。DataPipelineDecorator
1赞 lz96 8/16/2016 #7

我为这个问题实现了一个库,它支持类似管道的参数 passing&check。

演示

{{define "foo"}}
    {{if $args := . | require "arg1" | require "arg2" "int" | args }}
        {{with .Origin }} // Original dot
            {{.Bar}}
            {{$args.arg1}}
        {{ end }}
    {{ end }}
{{ end }}

{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" 42 }}
{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" "42" }} // will raise an error

Github 存储库

1赞 Igli Hoxha 9/13/2017 #8

基于@tux21b

我改进了该函数,因此即使不指定索引也可以使用它(只是为了保持将变量附加到模板的方式)

所以现在你可以这样做:

{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}

{{template "userlist" dict .MostPopular .CurrentUser}}

{{template "userlist" dict .MostPopular "Current" .CurrentUser}}

但是,如果参数 (.CurrentUser.name) 不是数组,则肯定需要放置索引才能将此值传递给模板

{{template "userlist" dict .MostPopular "Name" .CurrentUser.name}}

查看我的代码:

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
    "dict": func(values ...interface{}) (map[string]interface{}, error) {
        if len(values) == 0 {
            return nil, errors.New("invalid dict call")
        }

        dict := make(map[string]interface{})

        for i := 0; i < len(values); i ++ {
            key, isset := values[i].(string)
            if !isset {
                if reflect.TypeOf(values[i]).Kind() == reflect.Map {
                    m := values[i].(map[string]interface{})
                    for i, v := range m {
                        dict[i] = v
                    }
                }else{
                    return nil, errors.New("dict values must be maps")
               }
            }else{
                i++
                if i == len(values) {
                    return nil, errors.New("specify the key for non array values")
                }
                dict[key] = values[i]
            }

        }
        return dict, nil
    },
}).ParseGlob("templates/*.html")
3赞 Yuri 7/30/2018 #9

最直接的方法(尽管不是最优雅的)——尤其是对于一个相对较新的人来说——是“即时”使用匿名结构。早在 Andrew Gerrand 2012 年的出色演讲“你可能不知道的 10 件事”中,就已经记录/建议了这一点

https://talks.golang.org/2012/10things.slide#1

下面的简单例子:

// define the template

const someTemplate = `insert into {{.Schema}}.{{.Table}} (field1, field2)
values
   {{ range .Rows }}
       ({{.Field1}}, {{.Field2}}),
   {{end}};`

// wrap your values and execute the template

    data := struct {
        Schema string
        Table string
        Rows   []MyCustomType
    }{
        schema,
        table,
        someListOfMyCustomType,
    }

    t, err := template.New("new_tmpl").Parse(someTemplate)
    if err != nil {
        panic(err)
    }

    // working buffer
    buf := &bytes.Buffer{}

    err = t.Execute(buf, data)

请注意,从技术上讲,这不会按原样运行,因为模板需要一些小的清理(即删除范围循环最后一行的逗号),但这相当微不足道。将模板的参数包装在匿名结构中可能看起来很乏味和冗长,但它还有一个额外的好处,即明确说明模板执行后将使用的内容。绝对没有为你编写的每个新模板定义一个命名结构那么乏味。

2赞 Josh Bleecher Snyder 6/26/2022 #10

根据您的目标,https://github.com/josharian/tstruct博客文章)可能会有所帮助。您将定义一个名为 的 Go 结构体,使用 tstruct 为其自动生成 FuncMap 助手,然后编写如下内容:UserList

{{ template "userlist" UserList (Users .MostPopular) (Current .CurrentUser) }}
0赞 Igor A. Melekhine 8/15/2023 #11

您可以仅通过点将所有数据传递给它。.

{{ template "some" . }}