目录
html/template包实现了数据驱动的模板,用于生成可防止代码注入的安全的HTML内容。它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用html/template这个包。
模板与渲染
在一些前后端不分离的Web架构中,通常需要在后端将一些数据渲染到HTML文档中,从而实现动态的网页(网页的布局和样式大致一样,但展示的内容并不一样)效果。
很多编程语言的Web框架中都使用各种模板引擎,比如Python语言中Flask框架中使用的jinja2模板引擎。
Go语言的模板引擎
Go语言内置了文本模板引擎text/template和用于HTML文档的html/template。它们的作用机制可以简单归纳如下:
- 模板文件通常定义为
.tmpl
和.tpl
为后缀(也可以使用其他的后缀),必须使用UTF8编码。 - 模板文件中使用{{和}}包裹和标识需要传入的数据。
- 传给模板这样的数据就可以通过点号(
.
)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}
来访问它的字段。 - 除
{{
和}}
包裹的内容外,其他内容均不做修改原样输出。
模板引擎的使用
Go语言模板引擎的使用可以分为三部分:定义模板文件、解析模板文件和模板渲染.
定义模板文件
按照Go模板语法定义一个hello.tmpl的模板文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
<p>Hello {{.}}</p>
</body>
</html>
通过将模板应用于一个数据结构(即该数据结构作为模板的参数)来执行,来获得输出。模板中的注释引用数据接口的元素(一般如结构体的字段或者字典的键)来控制执行过程和获取需要呈现的值。模板执行时会遍历结构并将指针表示为’.‘(称之为”dot”)指向运行过程中数据结构的当前位置的值。
用作模板的输入文本必须是utf-8编码的文本。”Action”—数据运算和控制单位—由”“界定;在Action之外的所有文本都不做修改的拷贝到输出中。Action内部不能有换行,但注释可以有换行。
func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./hello.html")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
// 利用给定数据渲染模板,并将结果写入w
tmpl.Execute(w, "5lmh.com")
}
解析模板文件
可以使用下面的常用方法去解析模板文件,得到模板对象:
func (t *Template) Parse(src string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
当然,也可以使用func New(name string) *Template函数创建一个名为name的模板,然后对其调用上面的方法去解析模板字符串或模板文件。
// 创建模板对象,并为其添加一个模板名称
func New(name string) *Template {}
// 解析字符串
// 可以使用template.New("name").Parse(src string)
// 来创建模板对象,并完成解析模板内容。
func (t *Template) Parse(src string) (*Template, error) {}
// ParseFiles 方法可以解析模板文件,并得到模板对象
func ParseFiles(filenames ...string) (*Template, error) {}
// ParseGlob方法用于批量解析文件
// 比如在当前目录下有以h开头的模板10个
// 使用template.ParseGlob("h*")即可页将10个模板文件一起解析出来
func ParseGlob(pattern string) (*Template, error) {}
使用New在创建时就为其添加一个模板名称,并且执行t.Execute()会默认去寻找该名称进行数据融合。
使用ParseFiles一次指定多个文件加载多个模板进来,就不可以使用t.Execute()来执行数据融合,可以通过t.ExecuteTemplate()方法指定模板名称来执行数据融合。
模板渲染
渲染模板简单来说就是使用数据去填充模板,当然实际上可能会复杂很多。
func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
以上文定义的模板为例,创建一个main.go文件,在其中写下HTTP server端代码如下:
// main.go
func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
// 利用给定数据渲染模板,并将结果写入w
tmpl.Execute(w, "德玛西亚")
}
func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9090", nil)
if err != nil {
fmt.Println("HTTP server failed,err:", err)
return
}
}
将上面的main.go文件编译执行,然后使用浏览器访问http://127.0.0.1:9090就能看到页面上显示了“Hello 德玛西亚”。
模板语法
{{.}}
模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。
当传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:
type UserInfo struct {
Name string
Gender string
Age int
}
func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./hello.html")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
// 利用给定数据渲染模板,并将结果写入w
user := UserInfo{
Name: "枯藤",
Gender: "男",
Age: 18,
}
tmpl.Execute(w, user)
}
模板文件hello.tmpl内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
<p>Hello {{.Name}}</p>
<p>性别:{{.Gender}}</p>
<p>年龄:{{.Name}}</p>
</body>
</html>
同理,当传入的变量是map时,也可以在模板文件中通过.根据key来取值。
注释
{{/* a comment */}}
注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。
pipeline
pipeline是指产生数据的操作。比如{{.}}、{{.Name}}等。
Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。
注意 : 并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。
变量
Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:
$variable := pipeline
举例:
{{ $obj := "jack" }}
{{ $obj }} // 输出:jack
其中$variable是变量的名字。声明变量的行为不会产生任何输出。
移除空格
有时候=在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。
例如:
{{- .Name -}}
注意:-
要紧挨{{
和}}
,同时与模板值之间需要使用空格分隔。
条件判断
Go模板语法中的条件判断有以下几种:
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
range
Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。
{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出
{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。
with
{{with pipeline}} T1 {{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。
{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。
预定义函数
执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。
预定义的全局函数如下:
and
函数返回它的第一个empty参数或者最后一个参数;
就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
返回第一个非empty参数或者最后一个参数;
亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
返回它的单个参数的布尔值的否定
len
返回它的参数的整数类型长度
index
执行结果为第一个参数以剩下的参数为索引/键指向的值;
如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
即fmt.Sprint
printf
即fmt.Sprintf
println
即fmt.Sprintln
html
返回其参数文本表示的HTML逸码等价表示。
urlquery
返回其参数文本表示的可嵌入URL查询的逸码等价表示。
js
返回其参数文本表示的JavaScript逸码等价表示。
call
执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
其中Y是函数类型的字段或者字典的值,或者其他类似情况;
call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误
比较函数
布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
eq 如果arg1 == arg2则返回真
ne 如果arg1 != arg2则返回真
lt 如果arg1 < arg2则返回真
le 如果arg1 <= arg2则返回真
gt 如果arg1 > arg2则返回真
ge 如果arg1 >= arg2则返回真
为了简化多参数相等检测,eq(只有eq)可以接受2个或更多个参数,它会将第一个参数和其余参数依次比较,返回下式的结果:
{{eq arg1 arg2 arg3}}
比较函数只适用于基本类型(或重定义的基本类型,如”type Celsius float32”)。但是,整数和浮点数不能互相比较。
自定义函数
Go的模板支持自定义函数。
func sayHello(w http.ResponseWriter, r *http.Request) {
htmlByte, err := ioutil.ReadFile("./hello.tmpl")
if err != nil {
fmt.Println("read html failed, err:", err)
return
}
// 自定义一个夸人的模板函数
kua := func(arg string) (string, error) {
return arg + "真帅", nil
}
// 采用链式操作在Parse之前调用Funcs添加自定义的kua函数
tmpl, err := template.New("hello").Funcs(template.FuncMap{"kua": kua}).Parse(string(htmlByte))
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
user := UserInfo{
Name: "小王子",
Gender: "男",
Age: 18,
}
// 使用user渲染模板,并将结果写入w
tmpl.Execute(w, user)
}
可以在模板文件hello.tmpl中按照如下方式使用我们自定义的kua函数了。
{{kua .Name}}
嵌套template
可以在template中嵌套其他的template。这个template可以是单独的文件,也可以是通过define定义的template。
举个例子: t.tmpl文件内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>tmpl test</title>
</head>
<body>
<h1>测试嵌套template语法</h1>
<hr>
{{template "ul.html"}}
<hr>
{{template "ol.html"}}
</body>
</html>
{{ define "ol.html"}}
<h1>这是ol.html</h1>
<ol>
<li>吃饭</li>
<li>睡觉</li>
<li>打豆豆</li>
</ol>
{{end}}
ul.tmpl文件内容如下:
<ul>
<li>注释</li>
<li>日志</li>
<li>测试</li>
</ul>
注册一个templDemo路由处理函数.
http.HandleFunc("/tmpl", tmplDemo)
tmplDemo函数的具体内容如下:
func tmplDemo(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
user := UserInfo{
Name: "小王子",
Gender: "男",
Age: 18,
}
tmpl.Execute(w, user)
}
注意:在解析模板时,被嵌套的模板一定要在后面解析,例如上面的示例中t.tmpl模板中嵌套了ul.tmpl,所以ul.tmpl要在t.tmpl后进行解析。
block
{{block "name" pipeline}} T1 {{end}}
block是定义模板{{define “name”}} T1 {{end}}和执行{{template “name” pipeline}}缩写,典型的用法是定义一组根模板,然后通过在其中重新定义块模板进行自定义。
定义一个根模板templates/base.tmpl,内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
{{block "content" . }}{{end}}
</div>
</body>
</html>
然后定义一个templates/index.tmpl,“继承”base.tmpl:
{{template "base.tmpl"}}
{{define "content"}}
<div>Hello world!</div>
{{end}}
然后使用template.ParseGlob按照正则匹配规则解析模板文件,然后通过ExecuteTemplate渲染指定的模板:
func index(w http.ResponseWriter, r *http.Request){
tmpl, err := template.ParseGlob("templates/*.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
err = tmpl.ExecuteTemplate(w, "index.tmpl", nil)
if err != nil {
fmt.Println("render template failed, err:", err)
return
}
}
如果我们的模板名称冲突了,例如不同业务线下都定义了一个index.tmpl模板,我们可以通过下面两种方法来解决。
- 在模板文件开头使用
{{define 模板名}}
语句显式的为模板命名。 - 可以把模板文件存放在templates文件夹下面的不同目录中,然后使用
template.ParseGlob("templates/**/*.tmpl")
解析模板。
修改默认的标识符
Go标准库的模板引擎使用的花括号{{和}}作为标识,而许多前端框架(如Vue和 AngularJS)也使用{{和}}作为标识符,所以同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")
text/template与html/tempalte的区别
html/template针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击。
例如,定义下面的模板文件:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
{{.}}
</body>
</html>
这个时候传入一段JS代码并使用html/template去渲染该文件,会在页面上显示出转义后的JS内容。 <script>alert('嘿嘿嘿')</script>
这就是html/template做的事。
但是在某些场景下,如果相信用户输入的内容,不想转义的话,可以自行编写一个safe函数,手动返回一个template.HTML类型的内容。示例如下:
func xss(w http.ResponseWriter, r *http.Request){
tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{
"safe": func(s string)template.HTML {
return template.HTML(s)
},
}).ParseFiles("./xss.tmpl")
if err != nil {
fmt.Println("create template failed, err:", err)
return
}
jsStr := `<script>alert('嘿嘿嘿')</script>`
err = tmpl.Execute(w, jsStr)
if err != nil {
fmt.Println(err)
}
}
这样我们只需要在模板文件不需要转义的内容后面使用我们定义好的safe函数就可以了。
{{ . | safe }}
示例
func main(){
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
if request.Method=="GET"{
html,_:=template.ParseFiles("templates/index.tmpl")
data:=make([]map[interface{}]interface{},0,10)
for i:=0;i<10;i++{
data=append(data, map[interface{}]interface{}{
"id":i+1,
"message":[]interface{}{
"唱","跳","rap","篮球",
},
"data": map[string]interface{}{
"name":"123",
"age":rand.Int(),
},
})
}
err := html.Execute(writer, data)
if err != nil {
return
}
}
})
_=http.ListenAndServe("127.0.0.1:8080",nil)
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
{{ range $index,$value :=. }}
<p>
<p>id:
<span>{{ $value.id }}</span>
</p>
<p>message:
{{ range $i,$item:= $value.message}}
<span>{{ $item }} </span>
{{ end }}
</p>
<p>data:
<span>{{ $value.data.name }}</span>
<br>
<span>{{ $value.data.age }}</span>
</p>
</p>
{{ end }}
</body>
</html>
静态文件服务
当你在URL路径中使用"api/v1/static/"
时,需要确保路径匹配和剥离的设置正确。
http.Handle
的第一个参数应该是以斜杠结尾的路径,这样才能正确匹配到请求的URL。此外,路径剥离的功能也需要确保路径正确。
package main
import (
"log"
"net/http"
)
func main() {
// 设置静态文件服务的目录
fs := http.FileServer(http.Dir("static"))
// 将URL的路径"/api/v1/static/"映射到静态文件服务上
// 注意:这里的"/api/v1/static/"是URL路径,而不是文件系统的路径
http.Handle("/api/v1/static/", http.StripPrefix("/api/v1/static/", fs))
// 启动HTTP服务器,默认监听在80端口
log.Fatal(http.ListenAndServe(":80", nil))
}
修改后的代码中:
http.Handle
的第一个参数为"/api/v1/static/"
,确保路径正确匹配。http.StripPrefix
的参数也修正为"/api/v1/static/"
,确保剥离路径正确。
这样,当访问http://yourserver/api/v1/static/yourfile
时,Go服务器将会正确地将请求映射到static/yourfile
。
http.StripPrefix("/api/v1/static/", fs)
的作用是将请求 URL 中的 /api/v1/static/
前缀去掉,然后将剩余部分传递给文件服务器 fs
。
例如,如果用户请求 http://yourserver/api/v1/static/images/picture.jpg
:
http.StripPrefix("/api/v1/static/", fs)
会去掉 URL 中的/api/v1/static/
部分,剩下/images/picture.jpg
。- 然后将
/images/picture.jpg
传递给文件服务器fs
,文件服务器会在static
目录下查找这个文件,即static/images/picture.jpg
。
如果不使用 http.StripPrefix
,文件服务器会直接使用请求的路径 api/v1/static/images/picture.jpg
来查找文件。这意味着文件服务器会在你的静态文件目录中查找 static/api/v1/static/images/picture.jpg
,而不是你期望的 static/images/picture.jpg
。这就会导致找不到文件。
通过使用 http.StripPrefix
,可以让 URL 路径和文件系统路径保持一致,确保静态文件能够正确地被找到和提供。
总结来说,http.StripPrefix
用于去掉 URL 中的特定前缀,使得请求路径可以正确地映射到文件系统中的路径。