在Go模板中使用函数
在本教程中,我们将介绍如何使用模板函数,例如and
,eq
和index
为模板添加一些基本逻辑。一旦我们对如何使用这些功能有了很好的了解,我们将探索如何向模板添加一些自定义功能并使用它们。
本文是系列文章的一部分
这是一个四部分系列的第三部分,介绍了Go中的html / template
(和text / template
)软件包。如果您还没有,我建议您在这里查看本系列的其余部分:[Go中的模板简介。不需要阅读它们,但我认为您会喜欢它们。
如果您喜欢本系列,请考虑注册我的邮件列表在发布新文章时获得通知它。我保证我不会发垃圾邮件。
and
(与) 函数
默认情况下,模板中的if
操作将评估参数是否为空,但是当您要评估多个参数时会发生什么?您可以编写嵌套的if / else
块,但这会很快变得难看。
相反,html / template
包提供了and
函数。使用它与在Lisp(另一种编程语言)中使用and
函数的方式类似。这比说明容易显示,因此让我们跳入一些代码。打开main.go
并添加以下内容:
package main
import (
"html/template"
"net/http"
)
var testTemplate *template.Template
type User struct {
Admin bool
}
type ViewData struct {
*User
}
func main() {
var err error
testTemplate, err = template.ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":3000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
vd := ViewData{&User{true}}
err := testTemplate.Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
然后打开 hello.gohtml
并将以下内容添加到模板中。
{{if and .User .User.Admin}}
You are an admin user!
{{else}}
Access denied!
{{end}}
如果运行此代码,您应该会看到输出You are an admin user!
。如果将main.go
更新为不包含* User
对象,或者将Admin设置为false,或者即使您为提供
niltestTemplate.Execute()
方法,您将看到访问被拒绝!
。
and
函数接受两个参数,让它们分别称为a
和b
,然后运行大致等同于if a then b else a
。最奇怪的部分是and
实际上是一个函数,而不是放在两个变量之间的函数。只要记住这是一个函数而不是逻辑运算,就可以了。
同样,模板包还提供了or
函数,其功能类似于and
,不同之处在于它在为true则会短路。 即,如果a或a b不为空,则a或a b的逻辑大致相当于a则b不为空的情况下,b永远不会求值。
比较函数(等于,小于等)
到目前为止,我们一直在处理相对简单的逻辑,围绕某些东西是否为空,但是当我们需要进行一些比较时会发生什么呢?例如,如果我们想根据用户是否接近使用极限来调整对象上的类,该怎么办?
The html/template
包为我们提供了一些类来帮助进行比较。这些是
eq
- 返回的是布尔值arg1 == arg2
ne
- 返回的是布尔值arg1 != arg2
lt
- 返回的是布尔值arg1 < arg2
le
- 返回的是布尔值arg1 <= arg2
gt
- 返回的是布尔值arg1 > arg2
ge
- 返回的是布尔值arg1 >= arg2
它们的用法类似于使用与
和或
的方式,即首先键入函数,然后键入参数。例如,您可以在模板中使用以下代码来确定要根据API使用情况呈现哪些文本。
{{if (ge .Usage .Limit)}}
<p class="danger">
You have reached your API usage limit. Please upgrade or contact support for more help.
</p>
{{else if (gt .Usage .Warning)}}
<p class="warning">
You have used {{.Usage}} of {{.Limit}} API calls and are nearing your limit. Have you considered upgrading?
</p>
{{else if (eq .Usage 0)}}
<p>
You haven't used the API yet! What are you waiting for?
</p>
{{else}}
<p>
You have used {{.Usage}} of {{.Limit}} API calls.
</p>
{{end}}
if...else if...else
如果您一直关注该系列文章,那么还需要注意的是,该代码还演示了如何创建if ... elseif ... else
块,但我们尚未介绍。它们的工作原理很像if ... else
块,但是它们允许您使用一些不同的条件子句。
使用函数变量
到目前为止,我们大多数时候都在模板内部处理数据结构,但是如果我们想从模板内部调用自己的函数会怎样?例如,假设我们有一个User
类型,我们需要确定当前用户在创建UI时是否有权访问我们的仅企业功能。我们可以为视图创建一个客户结构,并为许可添加一个字段。
type ViewData struct {
Permissions map[string]bool
}
// or
type ViewData struct {
Permissions struct {
FeatureA bool
FeatureB bool
}
}
这种方法的问题在于,我们始终需要了解当前视图中使用的每个功能,或者如果我们改为使用map [string] bool
,则需要为每个视图填充一个值可能的功能。如果我们想知道用户是否可以访问某个功能,而只调用一个功能,将会容易得多。在Go中,有几种方法可以做到这一点,所以我将介绍几种可能的方法。
1.在User
类型上创建方法
第一个是最简单的-假设我们已经为视图提供了一个User
类型,我们可以向该对象添加一个HasPermission()
方法,然后使用它。要查看实际效果,请将以下内容添加到hello.gohtml
中。
{{if .User.HasPermission "feature-a"}}
<div class="feature">
<h3>Feature A</h3>
<p>Some other stuff here...</p>
</div>
{{else}}
<div class="feature disabled">
<h3>Feature A</h3>
<p>To enable Feature A please upgrade your plan</p>
</div>
{{end}}
{{if .User.HasPermission "feature-b"}}
<div class="feature">
<h3>Feature B</h3>
<p>Some other stuff here...</p>
</div>
{{else}}
<div class="feature disabled">
<h3>Feature B</h3>
<p>To enable Feature B please upgrade your plan</p>
</div>
{{end}}
<style>
.feature {
border: 1px solid #eee;
padding: 10px;
margin: 5px;
width: 45%;
display: inline-block;
}
.disabled {
color: #ccc;
}
</style>
然后将以下内容添加到同一目录下的main.go中。
package main
import (
"html/template"
"net/http"
)
var testTemplate *template.Template
type ViewData struct {
User User
}
type User struct {
ID int
Email string
}
func (u User) HasPermission(feature string) bool {
if feature == "feature-a" {
return true
} else {
return false
}
}
func main() {
var err error
testTemplate, err = template.ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":3000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
vd := ViewData{
User: User{1, "jon@calhoun.io"},
}
err := testTemplate.Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
运行代码后,您应该会在浏览器中看到类似以下内容:
我们成功地在前端启用和禁用了功能,具体取决于用户是否可以访问它们!当我们在类型上声明函数时,我们能够以与访问结构内部数据相同的方式调用这些函数,因此您对它们应该都感到非常熟悉。
现在我们已经了解了如何调用方法,让我们来看看使用call
函数在模板内部调用函数的更动态方式
2.调用函数变量和字段
让我们想象一下,由于某种原因,您无法使用上述方法,因为确定逻辑的方法有时需要更改。在这种情况下,有必要在User
类型上创建一个HasPermission func(string)bool
属性,然后为它分配一个函数。打开main.go
并更改代码以反映以下内容。
package main
import (
"html/template"
"net/http"
)
var testTemplate *template.Template
type ViewData struct {
User User
}
type User struct {
ID int
Email string
HasPermission func(string) bool
}
func main() {
var err error
testTemplate, err = template.ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":3000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
vd := ViewData{
User: User{
ID: 1,
Email: "jon@calhoun.io",
HasPermission: func(feature string) bool {
if feature == "feature-b" {
return true
}
return false
},
},
}
err := testTemplate.Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
看起来一切正常,但是如果启动服务器后在浏览器访问 localhost:3000 ,会发生下面的错误
template: hello.gohtml:1:10: executing "hello.gohtml" at <.User.HasPermission>: HasPermission has arguments but cannot be invoked as function
当我们将函数分配给变量时,我们需要告诉 html/template
包我们要调用该函数。打开hello.gohtml
文件,并在 if
语句后立即添加 call
一词,就像这样。
{{if (call .User.HasPermission "feature-a")}}
...
{{if (call .User.HasPermission "feature-b")}}
...
可以在模板中使用括号
尽管 Go 模板通常不需要解析,但它们对于明确将哪些参数传递给哪个函数以及指定明确的操作顺序非常有用。使用模板时请记住它们!
继续并重新启动服务器,然后再次浏览 localhost。您应该看到与以前相同的页面,但是这次启用了功能 B 而不是功能 A。
call
是 html/template
包已经提供的函数,该函数调用为其赋予的第一个参数(在本例中为 .User.HasPermission
函数)使用其余参数作为函数调用的参数。
3.使用 template.FuncMap
创建自定义函数
调用我们自己的函数的最后一种方法是使用 template.FuncMap
创建自定义函数。在我看来,这是定义函数的最有用和最强大的方法,因为它使我们能够创建可在整个应用程序中使用的全局帮助方法。
要开始使用,首先转到 template.FuncMap
的文档。首先要注意的是,这种类型似乎只是一个 map [string] interface {}
,但下面要注意的是,每个接口都必须是一个具有单个返回值的函数,或者是一个函数有两个返回值,第一个返回值是您需要在模板中访问的数据,第二个返回值是错误,如果不为 nil,它将终止模板执行。
刚开始时这可能会让人感到困惑,因此让我们仅举一个例子。再次打开 main.go
并更新它以匹配下面的代码。
package main
import (
"html/template"
"net/http"
)
var testTemplate *template.Template
type ViewData struct {
User User
}
type User struct {
ID int
Email string
}
func main() {
var err error
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(user User, feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":3000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
user := User{
ID: 1,
Email: "jon@calhoun.io",
}
vd := ViewData{user}
err := testTemplate.Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
然后再次打开 hello.gohtml
并更新每个 if 语句以使用新功能。
{{if hasPermission .User "feature-a"}}
...
{{if hasPermission .User "feature-b"}}
...
hasPermission
函数现在应该可以驱动您的逻辑以确定功能是否启用。在 main.go
中,我们定义了一个 template.FuncMap
,该映射将方法名称(“ hasPermission”
)映射到一个带有两个参数( User
和功能字符串),然后返回 true 或 false。然后我们调用 template.New()
函数来创建一个新模板,在此新模板上称为 Funcs()
方法来定义我们的自定义函数,然后我们最后解析了 hello.gohtml
文件作为模板的源。
在解析模板之前定义函数
在前面的示例中,我们通过调用 html/template
包提供的 template.ParseFiles
函数来创建模板。这是程序包级别的函数,在解析文件后返回模板。现在我们在 template.Template
类型上调用 ParseFiles方法,它具有相同的返回值但适用对现有模板(而不是全新模板)的更改,然后返回结果。
在这种情况下,我们需要使用该方法,因为我们需要首先定义计划在模板中使用的任何自定义函数,并且一旦使用模板包进行此操作,它将返回一个 * template.Template
。定义这些自定义函数后,我们可以继续解析使用这些函数的模板。如果我们要首先解析模板,则会看到与模板中未定义函数相关的错误。
接下来,我们将研究如何使此函数起作用,而不必在每次调用它时都传递 User
对象。
让我们的方法在全局可用
我们在上一节中定义的 hasPermission
函数很棒,但是它的一个问题是我们也只能在有权访问 User
对象的情况下使用它。一开始传递此消息可能并不坏,但是随着应用程序的增长,它最终将具有许多模板,并且很容易忘记将 User
对象传递给模板,或者错过它嵌套的模板。
如果我们可以简化它的功能,而只需要传入一个功能名称,那么我们的功能将简单得多,所以让我们继续更新我们的代码以实现这一点。
我们要做的第一件事是为没有 User
的情况下创建一个函数。我们将在解析模板之前在 template.FuncMap
中进行设置,以免出现解析错误,并确保在用户不可用时有适当的逻辑。
打开 main.go
并更新 main()
函数以匹配下面的代码。
func main() {
var err error
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
return false
},
}).ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":3000", nil)
}
接下来我们需要定义使用 closure 的函数。这基本上是一种幻想的说法,我们将定义一个动态函数,该函数可以访问不一定传递给它的变量,但是在定义函数时可用。在我们的例子中,该变量将是 User
对象。使用以下代码更新 main.go
内部的 handler()
函数。
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
user := User{
ID: 1,
Email: "jon@calhoun.io",
}
vd := ViewData{user}
//我们需要先克隆模板,然后再设置特定于用户的
// FuncMap 以避免任何潜在的竞争条件
err := template.Must(testTemplate.Clone()).Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
if user.ID == 1 && feature == "feature-a" {
return true
}
return false
},
}).Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
潜在的竞争条件!
这里应该注意的是,如果您没有在之前克隆 Funcs
来克隆模板,则可能会陷入竞争状态,其中多个 Web 请求都试图为模板设置不同的 FuncMaps 。最终结果可能是用户可以访问他们不应该访问的内容。这可能是有两个原因的:
- 默认情况下,Web 请求是在 goroutine 中处理的,因此您的服务器将自动同时自动处理多个请求。
- 我们将添加一个
FuncMap
,其闭包使用user
变量。在先前的示例中,我们将用户传递给了函数,因此无法实现这种竞争条件。
使用 Clone
可以很容易地解决这个问题,但是在代码中值得注意的是不要删除对 Clone
的调用。
想看更多的闭包示例吗?
如果您想对闭包有更多了解,包括其中一些使用中的示例,建议您阅读相关文章(单击下面的按钮)。在本文中,我解释了什么是匿名函数和闭包,并提供了示例,甚至还有后续文章,介绍了 Go 中闭包的常用用法。
即使我们在 main()
函数中定义了 hasPermission
函数,但是当我们可以访问 User
对象时,我们仍会在处理程序中覆盖它,但是在执行模板之前。这确实很强大,因为我们现在可以在任何模板中使用 hasPermission
函数,而不必担心 User
对象是否传递给模板了。
HTML 安全字符串和 HTML 注释
在 Go 中的模板简介-上下文编码中,我提到了是否需要防止某些 HTML 注释被剥离可以使用模板,但当时我们没有介绍如何做。在本节中,我们不仅将介绍如何实现此功能,还将讨论如何使任何字符串跳过执行 html/template
时发生的默认编码过程。
为了唤醒您的记忆,假设您的布局中有一些 HTML 需要注释以实现IE兼容性,如下所示。
<!--[if IE]>
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
<![endif]-->
不幸的是,html/template
软件包默认情况下会去除这些注释,因此我们需要想出一种使 HTML 安全的注释的方法。具体来说,我们需要创建一个为我们提供一个 template.HTML
对象的函数,该对象的内容为<!-[如果IE]>
,另一个为内容< ![endif]->
。
打开 main.go
并将其内容替换为以下内容。
package main
import (
"html/template"
"net/http"
)
var testTemplate *template.Template
func main() {
var err error
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"ifIE": func() template.HTML {
return template.HTML("<!--[if IE]>")
},
"endif": func() template.HTML {
return template.HTML("<![endif]-->")
},
}).ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":3000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
err := testTemplate.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
在 main 方法中,我们实现了我之前描述的功能,然后将其命名为 ifIE
和 endif
。这样我们就可以像这样更新模板(hello.gohtml
)。
{{ifIE}}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{{endif}}
然后,如果您重新启动服务器,请重新加载页面,然后查看页面源,您应该在其中看到以下内容:
<!--[if IE]>
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
<![endif]-->
程序工作的很好,但是如果我们可能会在应用程序中使用的每个注释创建函数将很快变得乏味。对于真正常见的注释(例如上面的 endif
),创建自己的函数是有意义的,但是我们需要一种方法来传递任何 HTML 注释并确保未对其进行编码。为此,我们需要定义一个接受字符串并将其转换为 template.HTML
的函数。再次打开 main.go
并更新您的模板.FuncMap
以匹配以下内容。
func main() {
// ...
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"ifIE": func() template.HTML {
return template.HTML("<!--[if IE]>")
},
"endif": func() template.HTML {
return template.HTML("<![endif]-->")
},
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
}).ParseFiles("hello.gohtml")
//...
}
使用我们新的 htmlSafe
函数,我们可以根据需要添加自定义注释,例如专门针对 IE6 的 if 语句。
{{htmlSafe "<!--[if IE 6]>"}}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{{htmlSafe "<![endif]-->"}}
由于我们仍然定义了该函数,因此本示例中的最后一行也可以是 {{endif}}
,但是为了保持一致性,我选择使用 htmlSafe
。
如果需要的话,我们的 htmlSafe
函数甚至可以与其他方法(例如 {{htmlSafe .User.Widget}}
)结合使用,但通常来说,如果您想要这些方法要返回 HTML 安全字符串,您可能应该将其返回类型更新为 template.HTML
,以便将来的开发人员明确您的意图。
总结
在完成所有示例之后,您应该对如何在模板中使用函数以及如何定义自己的函数并使它们可在模板内部进行访问具有扎实的了解。
在本系列的最后一篇文章中-在 MVC 中创建 V-我将介绍如何结合我们到目前为止,在本系列中已经学习到如何为 Web 应用程序创建可重用的视图层。我们甚至将开始使用 Bootstrap (一种流行的 HTML,CSS 和 JS 框架)使页面看起来更漂亮,以说明这如何不会影响其余的代码根本没有复杂性;相反,视图逻辑都与我们新创建的视图类型隔离。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: