Go的模板包(text/template
, html/template
)在很大程度上工作得很好。但是有一些问题很难/无法解决。
在当前实现中,一个Template
结构体在两个包中都有镜像,虽然有一些解析和执行的分离,以允许并发执行,但它们非常交织在一起。
这个提案建议添加一个新的“模板执行器”,它将:
- 准备并执行模板
- 存储和解析任何模板函数
- 提供一些用户定义的钩子接口,以允许自定义方法/函数/Map查找等的评估。
上述内容可能看起来像不同的事物,是不同提案的候选项。但这些问题是相互关联的,我相信即使这个提案只部分被接受,也应该把它们作为一个整体来看待。
上述的好处将是: - 有状态的函数。一个常见的例子将是一个
i18n
翻译函数。你可以把它做成一个方法({{ .I18n "hello" }}
),或者让它接受语言作为第一个参数({{ i18n .Language "hello" }}
),这两种都不实用。当前的解决方法是为每种语言克隆模板集,这既浪费资源,又在非平凡的设置中难以正确实现。我假设这些函数是在早期添加的,以获得解析时的签名验证,这是件好事,但我认为没有理由不使用另一个用于执行的集合,使函数查找成为性能上的一个巨大优势。 - 一些例子包括不区分大小写的Map查找、为支持它的方法添加一些执行上下文(proposal: text/template, html/template: add ExecuteContext methods #31107)、允许返回Map/切片范围的函数(text/template: Methods vs funcs discrepancy #20503)、允许自定义内置版本和相当主观的
IsTrue
版本(text/template: allow callers to override IsTrue #28391)。
这样做还可以消除一些性能瓶颈。在Hugo在我们做出这些更改之前/之后有一个相关的基准测试:
name old time/op new time/op delta
SiteNew/Many_HTML_templates-16 55.6ms ± 2% 42.9ms ± 1% -22.81% (p=0.008 n=5+5)
name old alloc/op new alloc/op delta
SiteNew/Many_HTML_templates-16 22.5MB ± 0% 17.6MB ± 0% -21.99% (p=0.008 n=5+5)
name old allocs/op new allocs/op delta
SiteNew/Many_HTML_templates-16 341k ± 0% 247k ± 0% -27.48% (p=0.008 n=5+5)
实现概述
在Hugo project中,我们解决了上面概述的问题,有些解决方法不太漂亮。但最近我们遇到了一个我们找不到解决办法的障碍,所以我们闭上眼睛创建了一个脚本分叉的text/template
和html/template
包。我们将引入上游修复,最终目标是让这个分叉消失。
但这意味着这个提案有一个可行的实现。这个实现有点受到尝试对现有代码做最少量的更改的影响,但我认为它概述了一个可能的API。而且它是向后兼容的。
// Executer executes a given template.
type Executer interface {
Execute(p Preparer, wr io.Writer, data interface{}) error
}
// Preparer prepares the template before execution.
type Preparer interface {
Prepare() (*Template, error)
}
// ExecHelper allows some custom eval hooks.
type ExecHelper interface {
GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value)
GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool)
IsTrue(val reflect.Value) bool
}
Hugo的分叉可以在这里找到 https://github.com/gohugoio/hugo/tree/master/tpl/internal/go_templates (所有的补丁都放在 hugo_*.go
文件中)。更改用注解 Added for Hugo.
标记。维护分叉的脚本可以在 https://github.com/gohugoio/hugo/tree/master/scripts/fork_go_templates 找到。
与Go相关的一些相关问题
- 添加ExecuteContext方法 proposal: text/template, html/template: add ExecuteContext methods #31107
- 通过再次调用Funcs来记录修改FuncMap的能力,以便在模板解析后进行文档 html/template: document ability to modify FuncMap after template parse by calling Funcs again #34680
- 让调用者覆盖IsTrue text/template: allow callers to override IsTrue #28391
- 方法与funcs之间的差异 text/template: Methods vs funcs discrepancy #20503
- index应该返回nil而不是索引超出范围错误 text/template: index should return nil instead of index out of range error #14751
8条答案
按热度按时间bttbmeg01#
Hugo为Go语言的荣耀做出了巨大贡献,因此从 Hugo开发人员那里解除维护单独分支的负担是很好的,因为这个建议解决了在更广泛范围内可能有用的问题。
rsl1atfo2#
我对这个提案的第一印象是,它打算对API进行全面改革,使包的功能更强大。听起来更像是一个v2模块,甚至是单独的模块,而不是我们可以对现有包做的一些增量更改的小集合。
有状态函数。[...]你可以将其变成一个方法[...],但这并不十分实用
你能详细解释一下吗?我认为对于大多数人来说,将方法作为解决方案可能是正确的选择。
为支持它的方法添加一些执行上下文,[...]允许内置的自定义变体和相当固执己见的
IsTrue
版本第一个提案似乎有可能以某种形式被接受,而第二个提案有一个尚未实现的解决方案。你为什么更倾向于这个较大的提案,而不是这两个较小的、逐步的提案呢?
虽然它们最近都没有进展,但这并不意味着它们被拒绝或不太可能发生。我们的努力可以集中在这些地方。
这样做还可以消除一些性能瓶颈。
有人调查过我们是否可以在不触及API的情况下获得任何性能提升吗?例如,市面上有数十个具有非常不同API的
encoding/json
替代品,但在Go的最近版本中,我们只是通过内部增量更改来提高性能10-30%。我认为在想要提高性能时,API更改是最后的手段。u4vypkhs3#
你能详细解释一下吗?我认为对于大多数人来说,方法可能是正确的解决方案。
这些函数代表了我所谓的交叉关注点(字符串翻译、日志记录、缓存等)。在Go模板中,方法API仅限于传递给
Execute
的"dot",以及传递给任何嵌套模板定义的"dot"。在Hugo中,我们有诸如Page
、Image
、Site
等接口,虽然我猜在简单的情况下,将一些I18nHelper
或其他东西嵌入到所有这些中是可能的,但我不认为这会实际可行/可能/美观。你需要考虑调用其他模板的模板,然后以某种方式 Package 原始上下文......即使是我想到这一点,也会让我头晕目眩,然后再想想普通人。你为什么更喜欢这个较大的提案而不是这两个较小的增量提案?
有两个原因:1)我相信将这个问题作为一个整体来看待,会给出一个总体上更好、更灵活的设计。我不是说我概述的设计很棒,但它至少是一个连贯的起点。2)这些较小的增量提案往往进展缓慢。坐等这些从Go 1.10到Go 1.20慢慢到位的过程并不好,如果你真的需要它们的话。
至于具体的
IsTrue
讨论。如果我没记错的话,提议的解决方案是将其变成一个可以被覆盖的模板函数。在我看来,这些函数/方法不应该从模板中调用,所以把它们放在那里听起来像是方便的事情,对我来说,更好的做法是定义一个或多个具有正确GoDoc等的接口。我认为API更改是在想要提高性能时最后的手段。
“几乎所有的性能问题都可以并且应该通过重新设计来解决,而不是优化。优化技巧应该是你最后求助的工具(s),而不是第一个。” - 引用Peter Bourgon在Twitter上的名言。我不同意他说的一切,但这一点我是认同的。
eni9jsuy4#
@bep 我查看了你在你的分支中创建的API,文档似乎有点缺乏。例如,这个函数是做什么的?
如果你能在这个问题中提供整个API的完整摘要,那将很有帮助,这样我们就不需要查看代码来尝试了解更改是什么以及它们的作用。
谢谢。
vsmadaxz5#
@bep AFAICS,您的大部分担忧(除了已经有一个接受的解决方案的 #28391 和无论如何都有一个良好解决方法的 #20503)可以通过实现 #31107 来解决,以允许“静默”地将
context.Context
传递给任何将上下文作为第一个参数的函数。这是否是一个合理的描述?如果您还有其他您想到的使用场景,在这里列出它们可能会很好。
bqjvbblv6#
你是否考虑过在自己的仓库中发布你的分叉包?
吸引用户是证明你的设计更好的最好方法,而说服Go团队修复/增强标准库API需要花费一些时间:-p
byqmnocz7#
我认为你错过了这个问题的主要主题:更严格的解析和执行分离,以允许有状态的函数,这也将消除相当昂贵的RLock/RUnlock结构,如果你将#28391实现为模板函数,它不会变得更好——这意味着每个条件(if,with等)都将受到互斥锁的保护。我猜想,对于Go的
if
语句的类似提议将被拒绝。我会在接下来的几天里找时间为Hugo的分支做更好的GoDoc。但是自定义代码应该在2个文件中的50-100行Go代码内,所以即使没有它也应该可以理解。
@networkimprov我之所以将这个放到Hugo的内部包中,是因为有一个原因。把它放在外面会1)使回到一个源代码真实性(这个问题是关于的)变得更加困难,2)它会增加我现在没有时间完成的工作量。但如果这个提议能可能引导出一个替代设计,允许模板执行生活在stdlib之外的包中,我会很欢迎。
vlf7wbxs8#
2020年1月10日,星期五,晚上11:03,Bjørn Erik Pedersen发邮件给rogpeppe说:
我认为你错过了这个问题的主要主题:更严格的解析和执行分离,以允许有状态的函数。
你可能是指每次模板运行时可以具有不同隐式状态的函数吗?这难道不是通过隐式上下文参数(尽管需要动态类型转换)来解决的问题吗?这样也可以消除相当昂贵的RLock/RUnlock结构,而且如果你将#28391 < #28391 >实现为模板函数,那么每个条件执行(if、with、not等)都将受到互斥锁的保护。我不明白这一点。
我认为用Go的if语句做类似的处理会被拒绝。我会在接下来的几天里找时间改进Hugo的fork的GoDoc。但是自定义代码应该在两个文件中包含50-100行Go代码,所以即使不看它也应该可以理解。@networkimprov我之所以将这个放在Hugo的内部包中,是因为这样做的原因。把它放在外面会带来以下问题:1)让它更难回到一个源代码的真相(这个问题就是关于的);2)它会增加我现在没有时间完成的工作量。——你收到这封邮件是因为有人提到了你。直接回复这封邮件,或者在GitHub上查看<#36462?email_source=notifications&email_token=AAAQHO4WBVTPPRSDJI7KNWTQ5D5E5A5CNFSM4KEM2LCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEIVPDAQ#issuecomment-573239682>,或者取消订阅< https://github.com/notifications/unsubscribe-auth/AAAQHOYALF7AEBZL5WNW54LQ5D5E5ANCNFSM4KEM2LCA >(如果已经订阅的话)。