0
点赞
收藏
分享

微信扫一扫

go template语法:解析和创建模板,模板变量,模板动作,模板函数,模板比较函数,嵌套模板和布局,模板调用函数


Go标准库提供了几个package可以产生输出结果,而​​text/template ​​​提供了基于模板输出文本内容的功能。​​html/template​​则是产生 安全的HTML格式的输出。这两个包使用相同的接口,但是我下面的例子主要面向HTML应用。

解析和创建模板

命名模板

模板没有限定扩展名,最流行的后缀是​​.tmpl​​​, vim-go提供了对它的支持,并且godoc的例子中也使用这个​​后缀​​​。Atom 和 GoSublime 对​​.gohtml​​​后缀的文件提供了语法高亮的支持。通过对代码库的分析统计发现​​.tpl​​后缀也被经常使用。当然后缀并不重要,在项目中保持清晰和一致即可。

创建模板

​tpl, err := template.Parse(filename)​​​得到文件名为名字的模板,并保存在​​tpl​​变量中。tpl可以被执行来显示模板。

解析多个模板

​template.ParseFiles(filenames)​​​可以解析一组模板,使用文件名作为模板的名字。​​template.ParseGlob(pattern)​​​会根据​​pattern​​解析所有匹配的模板并保存。

解析字符串模板

​t, err := template.New("foo").Parse(\​​​{ {define “T”}}Hello, { {.}}!{ {end}}​​)​​ 可以解析字符串模板,并设置它的名字。

执行模板

执行简单模板

又两种方式执行模板。简单的模板​​tpl​​​可以通过​​tpl.Execute(io.Writer, data)​​​去执行, 模板渲染后的内容写入到​​io.Writer​​​中。​​Data​​是传给模板的动态数据。

执行命名的模板

​tpl.ExecuteTemplate(io.Writer, name, data)​​​和上面的简单模板类似,只不过传入了一个模板的名字,指定要渲染的模板(因为​​tpl​​可以包含多个模板)。

模板编码和HTML

上下文编码

​html/template​​基于上下文信息进行编码,因此任何需要编码的字符都能被正确的进行编码。

例如​​"<h1>A header!</h1>"​​​中的尖括号会被编码为​​&lt;h1&gt;A header!&lt;/h1&gt;​​。

​template.HTML​​​可以告诉Go要处理的字符串是安全的,不需要编码。​​template.HTML("<h1>A Safe header</h1>")​​​会输出​​<h1>A Safe header</h1>​​,注意这个方法处理用户的输入的时候比较危险。

​html/template​​还可以根据模板中的属性进行不同的编码。(The go html/template package is aware of attributes within the template and will encode values differently based on the attribute.)

Go 模板也可以应用javascript。struct和map被展开为JSON 对象,引号会被增加到字符串中,,用做函数参数和变量的值。

// Go
type Cat struct {
Name string
Age int
}

kitten := Cat{"Sam", 12}

// Template
<script>
var cat = { {.kitten}}
</script>

// Javascript
var cat = {"Name":"Sam", "Age" 12}

安全字符串和 HTML注释

默认情况下 ​​html/template​​会删除模板中的所有注释,这会导致一些问题,因为有些注释是有用的,比如:

<!--[if IE]>
Place content here to target all Internet Explorer users.
<![endif]-->

我们可以使用自定义的方法创建一个可以返回注释的函数。在FuncMap中定义​​htmlSafe​​方法:

testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"htmlSafe": func(html string) template.HTML {
return template.HTML(html)
},
}).ParseFiles("hello.gohtml")

这个函数会产生一模一样的HTML代码,这个函数可以用在模板中保留前面的注释:

{ {htmlSafe "<!--[if IE 6]>" }}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{ { htmlSafe "<![endif]-->" }}

模板变量

. 字符

模板变量可以是boolean, string, character, integer, floating-point, imaginary 或者 complex constant。传给模板这样的数据就可以通过点号​​.​​来访问:

{ { . }}

如果数据是复杂类型的数据,可以通过​​{ { .FieldName }}​​来访问它的字段。

如果字段还是复杂类型,可以链式访问 ​​{ { .Struct.StructTwo.Field }}​​。

模板中的变量

传给模板的数据可以存在模板中的变量中,在整个模板中都能访问。 比如 ​​{ {$number := .}}​​​, 我们使用​​$number​​​作为变量,保存传入的数据,可以使用​​{ {$number}}​​来访问变量。

{ {$number := .}}
<h1> It is day number { {$number}} of the month </h1>
var tpl *template.Template

tpl = template.Must(template.ParseFiles("templateName"))

err := tpl.ExecuteTemplate(os.Stdout, "templateName", 23)

上面的例子我们把23传给模板,模板的变量​​$number​​的值是23,可以在模板中使用。

模板动作

if/else 语句

像其它语言,模板支持​​if/else​​语句。我们可以使用if检查数据,如果不满足可以执行else。空值是是false, 0、nil、空字符串或者长度为0的字符串都是false。

<h1>Hello, { {if .Name}} { {.Name}} { {else}} Anonymous { {end}}!</h1>

如果​​.Name​​​存在,会输出​​Hello, Name​​​,否则输出​​Hello, Anonymous​​。

模板也提供了​​{ {else if .Name2 }}​​处理多个分支。

移除空格

往模板中增加不同的值的时候可能会增加一定数量的空格。我们既可以改变我们的模板以便更好的处理它,忽略/最小化这种效果,或者我们还可以使用减号​​-​​:

<h1>Hello, { {if .Name}} { {.Name}} { {- else}} Anonymous { {- end}}!</h1>

上面的例子告诉模板移除 ​​.Name​​变量之间的空格。我们在end关键字中也加入减号。这样做的好处是在模板中我们通过空格更方便编程调试,但是生产环境中我们不需要空格。

Range

模板提供​​range​​关键字来遍历数据。假如我们又下面的数据结构:

type Item struct {
Name string
Price int
}

type ViewData struct {
Name string
Items []Item
}

​ViewData​​对象传给模板,模板如下:

{ {range .Items}}
<div class="item">
<h3 class="name">{ {.Name}}</h3>
<span class="price">${ {.Price}}</span>
</div>
{ {end}}

对于Items中的每个Item, 我们输出它的名称和价格。在range中当前的项目变成了​​{ {.}}​​​,它的属性是​​{ {.Name}}​​​和​​{ {.Price}}​​。

模板函数

模板包提供了一组预定义的函数,下面介绍一些常用的函数。

获取索引值

如果传给模板的数据是map、slice、数组,那么我们就可以使用它的索引值。我们使用​​{ {index x number}}​​​来访问​​x​​​的第​​number​​​个元素, ​​index​​​是关键字。比如​​{ {index names 2}}​​​等价于​​names[2]​​​。​​{ {index names 2 3 4}}​​​ 等价于 ​​names[2][3][4]​​。

<body>
<h1> { {index .FavNums 2 }}</h1>
</body>
type person struct {
Name string
FavNums []int
}

func main() {

tpl := template.Must(template.ParseGlob("*.gohtml"))
tpl.Execute(os.Stdout, &person{"Curtis", []int{7, 11, 94}})
}

上面的例子传入一个person的数据结构,得到它的FavNums字段中的第三个值。

and 函数

and函数返回bool值,通过返回第一个空值或者最后一个值。​​and x y​​​逻辑上相当于​​if x then y else x​​。考虑下面的代码:

type User struct {  
Admin bool
}

type ViewData struct {
*User
}

传入一个Admin为true的ViewData对象给模板:

{ {if and .User .User.Admin}}
You are an admin user!
{ {else}}
Access denied!
{ {end}}

结果会显示​​You are an admin user!​​​, 如果ViewData不包含一个User值,或者Admin为false,显示结果则会是​​Access denied!​​。

or 函数

类似 and 函数,但是只要遇到 true就返回。​​or x y​​​ 等价于 ​​if x then x else y​​。 x 非空的情况下y不会被评估。

not 函数

not函数返回参数的相反值:

{ { if not .Authenticated}}
Access Denied!
{ { end }}

管道

函数调用可以链式调用,前一个函数的输出结果作为下一个函数调用的参数。​​html/template​​​称之为管道,类似于linux shell命令中的管道一样,它采用​​|​​分隔。

注意前一个命令的输出结果是作为下一个命令的最后一个参数,最终命令的输出结果就是这个管道的结果。

模板比较函数

比较

​html/template​​​提供了一系列的函数用做数据的比较。数据的类型只能是基本类型和命名的基本类型,比如​​type Temp float3​​​,格式是​​{ { function arg1 arg2 }}​​。

  • ​eq​​: arg1 == arg2
  • ​ne​​: arg1 != arg2
  • ​lt​​: arg1 < arg2
  • ​le​​: arg1 <= arg2
  • ​gt​​: arg1 > arg2
  • ​ge​​: arg1 >= arg2

​eq​​​函数比较特殊,可以拿多个参数和第一个参数进行比较。​​{ { eq arg1 arg2 arg3 arg4}}​​​逻辑是​​arg1==arg2 || arg1==arg3 || arg1==arg4​​。

嵌套模板和布局

嵌套模板

嵌套模板可以用做跨模板的公共部分代码,比如 header或者 footer。使用嵌套模板我们就可以避免一点小小的改动就需要修改每个模板。嵌套模板定义如下:

{ {define "footer"}}
<footer>
<p>Here is the footer</p>
</footer>
{ {end}}

这里定义了一个名为​​footer​​的模板,可以在其他模板中使用:

{ {template "footer"}}

模板之间传递变量

模板action可以使用第二个参数传递数据给嵌套的模板:

// Define a nested template called header
{ {define "header"}}
<h1>{ {.}}</h1>
{ {end}}

// Call template and pass a name parameter
{ {range .Items}}
<div class="item">
{ {template "header" .Name}}
<span class="price">${ {.Price}}</span>
</div>
{ {end}}

这里我们使用和上面一样的range遍历items,但是我们会把每个name传给header模板。

创建布局

Glob模式通过通配符匹配一组文件名。​​template.ParseGlob(pattern string)​​​会匹配所有符合模式的模板。​​template.ParseFiles(files...)​​​也可以用来解析一组文件。

模板默认情况下会使用配置的参数文件名的base name作为模板名。这意味着​​views/layouts/hello.gohtml​​​的文件名是​​hello.gohtml​​​,如果模板中有​​{ {define “templateName”}}​​​的话,那么​​templateName​​会用作这个模板的名字。

模板可以通过​​t.ExecuteTemplate(w, "templateName", nil)​​​来执行, t是一个类型为​​Template​​​的对象,​​w​​​的类型是​​io.Writer​​​,比如​​http.ResponseWriter​​,然后是要执行的模板的名称,以及要传入的数据:

main.go
// Omitted imports & package

var LayoutDir string = "views/layouts"
var bootstrap *template.Template

func main() {
var err error
bootstrap, err = template.ParseGlob(LayoutDir + "/*.gohtml")
if err != nil {
panic(err)
}

http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
bootstrap.ExecuteTemplate(w, "bootstrap", nil)
}

所有的​​.gohtml​​​文件都被解析,然后当访问​​/​​​的时候,​​bootstrap​​会被执行。

​views/layouts/bootstrap.gohtml​​定义如下:

views/layouts/bootstrap.gohtml
{ {define "bootstrap"}}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Go Templates</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<h1>Filler header</h1>
<p>Filler paragraph</p>
</div>
<!-- jquery & Bootstrap JS -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"
</script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js">
</script>
</body>
</html>
{ {end}}

模板调用函数

函数变量 (调用结构体的方法)

我们可以调用模板中对象的方法返回数据,下面定义了User类型,以及一个方法:

type User struct {  
ID int
Email string
}

func (u User) HasPermission(feature string) bool {
if feature == "feature-a" {
return true
} else {
return false
}
}

当User类型传给模板后,我们可以在模板中调用它的方法:

{ {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}}

模板会调用User的HasPermission方法做检查,并且根据这个返回结果渲染数据。

函数变量 (调用)

如果有时HasPermission方法的设计不得不需要更改,但是当前的函数方法有不满足要求,我们可以使用函数(​​func(string) bool​​)作为User类型的字段,这样在创建User的时候可以指派不同的函数实现:

// Structs
type ViewData struct {
User User
}

type User struct {
ID int
Email string
HasPermission func(string) bool
}

// Example of creating a ViewData
vd := ViewData{
User: User{
ID: 1,
Email: "curtis.vermeeren@gmail.com",
// Create the HasPermission function
HasPermission: func(feature string) bool {
if feature == "feature-b" {
return true
}
return false
},
},
}

// Executing the ViewData with the template
err := testTemplate.Execute(w, vd)

我们需要告诉Go模板我们想调用这个函数,这里使用​​call​​关键字。把上面的例子修改如下:

{ {if (call .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}}

自定义函数

另外一种方式是使用​​template.FuncMap​​​创建自定义的函数,它创建一个全局的函数,可以在整个应用中使用。​​FuncMap​​​通过​​map[string]interface{}​​​将函数名映射到函数上。注意映射的函数必须只有一个返回值,或者有两个返回值但是第二个是​​error​​类型。

// Creating a template with function hasPermission
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")

这个函数​​hasPermission​​​检查用户是否有某个权限,它会被保存在​​FuncMap​​​中。注意自定义的函数必须在调用​​ParseFiles()​​之前创建。

这个函数在模板中的使用如下:

{ { if hasPermission .User "feature-a" }}

需要传入​​.User​​​和​​feature-a​​参数。

自定义函数 (全局)

我们前面实现的自定义方法需要依赖​​.User​​类型,很多情况下这种方式工作的很好,但是在一个大型的应用中传给模板太多的对象维护起来很困难。我们需要改变自定义的函数,让它无需依赖User对象。

和上面的实现类似,我们创建一个缺省的​​hasPermission​​函数,这样可以正常解析模板。

testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
"hasPermission": func(feature string) bool {
return false
},
}).ParseFiles("hello.gohtml")

这个函数在​​main()​​中或者某处创建,并且保证在解析文件之前放入到 hello.gohtml 的function map中。这个缺省的函数总是返回false,但是不管怎样,函数是已定义的,而且不需要User,模板也可以正常解析。

下一个技巧就是重新定义​​hasPermission​​函数。这个函数可以使用User对象的数据,但是它是在Handler处理中使用的,而不是传给模板,这里采用的是闭包的方式。所以在模板执行之前你死有机会重新定义函数的。

func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")

user := User{
ID: 1,
Email: "Curtis.vermeeren@gmail.com",
}
vd := ViewData{}
err := testTemplate.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)
}
}

在这个Handler中User被创建,ViewData使用这个User对象。​​hasPermission​​​采用闭包的方式重新定义了函数。​​{ {if hasPermission "feature-a"}}​​的的确确没有传入User参数。

第三方自定义函数

除了官方的预定义的函数外,一些第三方也定义了一些函数,你可以使用这些库,避免重复造轮子。

比如​​sprig​​库,定义了很多的函数:

  • String Functions
    :

trim

,

wrap

,

randAlpha

,

plural

, etc.

  • ​​String List Functions​​​:​​splitList​​​,​​sortAlpha​​, etc.
  • Math Functions
    :

add

,

max

,

mul

, etc.

  • ​​Integer Slice Functions​​​:​​until​​​,​​untilStep​
  • ​​Date Functions​​​:​​now​​​,​​date​​, etc.
  • ​​Defaults Functions​​​:​​default​​​,​​empty​​​,​​coalesce​​​,​​toJson​​​,​​toPrettyJson​​​,​​toRawJson​​​,​​ternary​
  • ​​Encoding Functions​​​:​​b64enc​​​,​​b64dec​​, etc.
  • ​​Lists and List Functions​​​:​​list​​​,​​first​​​,​​uniq​​, etc.
  • ​​Dictionaries and Dict Functions​​​:​​get​​​,​​set​​​,​​dict​​​,​​hasKey​​​,​​pluck​​​,​​deepCopy​​, etc.
  • ​​Type Conversion Functions​​​:​​atoi​​​,​​int64​​​,​​toString​​, etc.
  • ​​File Path Functions​​​:​​base​​​,​​dir​​​,​​ext​​​,​​clean​​​,​​isAbs​
  • ​​Flow Control Functions​​​:​​fail​
  • Advanced Functions
  • ​​UUID Functions​​​:​​uuidv4​
  • ​​OS Functions​​​:​​env​​​,​​expandenv​
  • ​​Version Comparison Functions​​​:​​semver​​​,​​semverCompare​
  • ​​Reflection​​​:​​typeOf​​​,​​kindIs​​​,​​typeIsLike​​, etc.
  • ​​Cryptographic and Security Functions​​​:​​derivePassword​​​,​​sha256sum​​​,​​genPrivateKey​​, etc.

参考链接:​​https://colobu.com/2019/11/05/Golang-Templates-Cheatsheet​​


举报

相关推荐

0 条评论