跳到主要内容

Golang template

解析和创建模板

命名模板

模板没有限定扩展名,最流行的后缀是.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。传给模板这样的数据就可以通过点号`.`来访问:

1

{ { . }}

如果数据是复杂类型的数据,可以通过`{ { .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。

1

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

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

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

### 移除空格

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

1

\<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可以使用第二个参数传递数据给嵌套的模板:

1




// 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()`之前创建。

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

1

{ { 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处理中使用的,而不是传给模板,这里采用的是闭包的方式。所以在模板执行之前你死有机会重新定义函数的。

1


9

10

11

12

13

14

15

16

17

18

19

20

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](https://github.com/Masterminds/sprig)库,定义了很多的函数:
* [String Functions](https://github.com/Masterminds/sprig/blob/master/docs/strings.md): `trim`, `wrap`, `randAlpha`, `plural`, etc.
* [String List Functions](https://github.com/Masterminds/sprig/blob/master/docs/string_slice.md): `splitList`, `sortAlpha`, etc.
* [Math Functions](https://github.com/Masterminds/sprig/blob/master/docs/math.md): `add`, `max`, `mul`, etc.
* [Integer Slice Functions](https://github.com/Masterminds/sprig/blob/master/docs/integer_slice.md): `until`, `untilStep`
* [Date Functions](https://github.com/Masterminds/sprig/blob/master/docs/date.md): `now`, `date`, etc.
* [Defaults Functions](https://github.com/Masterminds/sprig/blob/master/docs/defaults.md): `default`, `empty`, `coalesce`, `toJson`, `toPrettyJson`, `toRawJson`, `ternary`
* [Encoding Functions](https://github.com/Masterminds/sprig/blob/master/docs/encoding.md): `b64enc`, `b64dec`, etc.
* [Lists and List Functions](https://github.com/Masterminds/sprig/blob/master/docs/lists.md): `list`, `first`, `uniq`, etc.
* [Dictionaries and Dict Functions](https://github.com/Masterminds/sprig/blob/master/docs/dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `deepCopy`, etc.
* [Type Conversion Functions](https://github.com/Masterminds/sprig/blob/master/docs/conversion.md): `atoi`, `int64`, `toString`, etc.
* [File Path Functions](https://github.com/Masterminds/sprig/blob/master/docs/paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`
* [Flow Control Functions](https://github.com/Masterminds/sprig/blob/master/docs/flow_control.md): `fail`
* Advanced Functions
* [UUID Functions](https://github.com/Masterminds/sprig/blob/master/docs/uuid.md): `uuidv4`
* [OS Functions](https://github.com/Masterminds/sprig/blob/master/docs/os.md): `env`, `expandenv`
* [Version Comparison Functions](https://github.com/Masterminds/sprig/blob/master/docs/semver.md): `semver`, `semverCompare`
* [Reflection](https://github.com/Masterminds/sprig/blob/master/docs/reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc.
* [Cryptographic and Security Functions](https://github.com/Masterminds/sprig/blob/master/docs/crypto.md): `derivePassword`, `sha256sum`, `genPrivateKey`, etc.