[译]Golang template 小抄
目录 [−]
function show_answer(btn, x) { if (btn.value === "显示答案") { btn.value = "隐藏答案" } else { btn.value = "显示答案" } var as = document.getElementById(x); if (as.style.display === "none") { as.style.display = "block" } else { as.style.display = "none" } }
这是 Curtis Vermeeren 做的 Go模板技术的小抄,这是我非常喜欢的形式,可以以一个简短的总结把相关的技术介绍出来。 大家可以看我以前翻译的Go文件操作大全,也是统一风格的文章。我翻译了这篇文章,补充了遗漏的知识点。
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>"
中的尖括号会被编码为<h1>A header!</h1>
。
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 对象,引号会被增加到字符串中,,用做函数参数和变量的值。
123456789101112131415 | // Gotype Cat struct { Name string Age int}kitten := Cat{"Sam", 12}// Template<script> var cat = { {.kitten}}</script>// Javascriptvar cat = {"Name":"Sam", "Age" 12} |
安全字符串和 HTML注释
默认情况下 html/template
会删除模板中的所有注释,这会导致一些问题,因为有些注释是有用的,比如:
123 | <!--[if IE]>Place content here to target all Internet Explorer users.<![endif]--> |
我们可以使用自定义的方法创建一个可以返回注释的函数。在FuncMap中定义htmlSafe
方法:
12345 | testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{ "htmlSafe": func(html string) template.HTML { return template.HTML(html) },}).ParseFiles("hello.gohtml") |
这个函数会产生一模一样的HTML代码,这个函数可以用在模板中保留前面的注释:
123 | { {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}}
来访问变量。
12 | { {$number := .}}<h1> It is day number { {$number}} of the month </h1> |
12345 | var tpl *template.Templatetpl = 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
关键字来遍历数据。假如我们又下面的数据结构:
12345678910 | type Item struct { Name string Price int}type ViewData struct { Name string Items []Item} |
ViewData
对象传给模板,模板如下:
123456 | { {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]
。
123 | <body> <h1> { {index .FavNums 2 }}</h1></body> |
12345678910 | 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
。考虑下面的代码:
1234567 | type User struct { Admin bool}type ViewData struct { *User} |
传入一个Admin为true的ViewData对象给模板:
12345 | { {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函数返回参数的相反值:
123 | { { if not .Authenticated}} Access Denied!{ { end }} |
管道
函数调用可以链式调用,前一个函数的输出结果作为下一个函数调用的参数。html/template
称之为管道,类似于linux shell命令中的管道一样,它采用|
分隔。
注意前一个命令的输出结果是作为下一个命令的最后一个参数,最终命令的输出结果就是这个管道的结果。
模板比较函数
比较
html/template
提供了一系列的函数用做数据的比较。数据的类型只能是基本类型和命名的基本类型,比如type Temp float3
,格式是{ { function arg1 arg2 }}
。
eq
: arg1 == arg2ne
: arg1 != arg2lt
: arg1 < arg2le
: arg1 <= arg2gt
: arg1 > arg2ge
: arg1 >= arg2
eq
函数比较特殊,可以拿多个参数和第一个参数进行比较。{ { eq arg1 arg2 arg3 arg4}}
逻辑是arg1==arg2 || arg1==arg3 || arg1==arg4
。
嵌套模板和布局
嵌套模板
嵌套模板可以用做跨模板的公共部分代码,比如 header或者 footer。使用嵌套模板我们就可以避免一点小小的改动就需要修改每个模板。嵌套模板定义如下:
12345 | { {define "footer"}}<footer> <p>Here is the footer</p></footer>{ {end}} |
这里定义了一个名为footer
的模板,可以在其他模板中使用:
1 | { {template "footer"}} |
模板之间传递变量
模板action可以使用第二个参数传递数据给嵌套的模板:
123456789101112 | // 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
12345678910111213141516171819 | // Omitted imports & packagevar LayoutDir string = "views/layouts" var bootstrap *template.Templatefunc 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
123456789101112131415161718192021 | { {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类型,以及一个方法:
123456789101112 | type User struct { ID int Email string}func (u User) HasPermission(feature string) bool { if feature == "feature-a" { return true } else { return false }} |
当User类型传给模板后,我们可以在模板中调用它的方法:
1234567891011 | { {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的时候可以指派不同的函数实现:
12345678910111213141516171819202122232425262728 | // Structstype ViewData struct { User User}type User struct { ID int Email string HasPermission func(string) bool}// Example of creating a ViewDatavd := ViewData{ User: User{ ID: 1, Email: "[email protected]", // Create the HasPermission function HasPermission: func(feature string) bool { if feature == "feature-b" { return true } return false }, }, }// Executing the ViewData with the templateerr := testTemplate.Execute(w, vd) |
我们需要告诉Go模板我们想调用这个函数,这里使用call
关键字。把上面的例子修改如下:
1234567891011 | { {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
类型。
123456789 | // Creating a template with function hasPermissiontestTemplate, 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
函数,这样可以正常解析模板。
12345 | 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处理中使用的,而不是传给模板,这里采用的是闭包的方式。所以在模板执行之前你死有机会重新定义函数的。
1234567891011121314151617181920 | func handler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") user := User{ ID: 1, Email: "[email protected]", } 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.
- String List Functions:
- Math Functions:
add
,max
,mul
, etc.- Integer Slice Functions:
until
,untilStep
- Integer Slice Functions:
- 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.
- UUID Functions: