Golang笔记-Typora2
Go语言标准库 - template
一、模板与渲染
在一些前后端不分离的 Web架构中,我们通常需要在后端将一些数据渲染到 HTML文档中,从而实现动态的网页(网页的布局和样式大致一样,但展示的内容并不一样)效果。
我们这里说的模板可以理解为事先定义好的 HTML文档文件,模板渲染的作用机制可以简单理解为文本替换操作–使用相应的数据去替换 HTML文档中事先准备好的标记。
很多编程语言的 Web框架中都使用各种模板引擎,比如 Python语言中Flask框架中使用的 jinja2模板引擎。
简单地说,渲染就是把后端变量中的数据显示在前端的html页面中
(1)模板引擎的使用
Go语言内置了文本模板引擎 text/template 和用于HTML文档的 html/template。它们的作用机制可以简单归纳如下:
- 模板文件通常定义为
.tmpl和.tpl为后缀(也可以使用其他的后缀),必须使用UTF8编码。 - 模板文件中使用
{{和}}包裹和标识需要传入的数据。 - 传给模板这样的数据就可以通过点号(
.)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。 - 除
{{和}}包裹的内容外,其他内容均不做修改原样输出。
(与python中的Flask框架用法类似,只是注意渲染需要分为若干小步骤)
1.定义模板文件
Go语言将扩展名为 .tmpl 和 .tpl 的文件作为模板文件
模板文件本质上仍是一个html文件,仅是把要传入的数据通过 {{.}} 包裹
// .tmpl 文件
<!DOCTYPE html>
<html lang="zh-CN">
<head><title>hello, this is a title</title></head>
<body>
<p>this is {{.}}</p>
</body>
</html>
2.解析模板文件
func (t *Template) Parse(src string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
解析函数的写法:
①
t, err := template.ParseFiles("./hello.tmpl")
②
t, err := template.New("hello.tmpl").ParseFiles("./hello.tmpl")
注意 New函数的参数一定要与待解析的 tmpl文件名一致!!
3.渲染模板文件
也即把模板文件中的 {{.}} 用传入的变量渲染
func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
- 举例:使用Go标准库 "net/http" 来实现模板渲染
package main
import (
"fmt"
"html/template"
"net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) {
//2.解析模板 - Parse
t, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Printf("parse template faild, err: %v", err)
return
}
//3.渲染模板 - Execute
name := "小王子"
err = t.Execute(w, name)
if err != nil {
fmt.Printf("parse template faild, err: %v", err)
return
}
}
func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9000", nil)
if err != nil {
fmt.Printf("http server start faild, err: %v", err)
return
}
}
(2)模板语法
1. {{ . }}
①当传入结构体时,可以用 .field 来访问结构体的字段内容
type UserInfo struct {
Name string
gender string
Age int
}
<!DOCTYPE html>
<html lang="zh-CN">
<head><title>hello, this is a title</title></head>
<body>
<p>名字:{{.Name}}</p>
<p>年龄:{{.Age}}</p>
<p>性别:{{.gender}}</p>
</body>
</html>
注意:Golang中当字段首字母为小写时,表示私有,外部函数、方法不可以访问。所以本例中性别的数值不会在网站中出现
②当传入的变量是map时,可以在模板文件中通过 .key 来取值
注意:因为map中键名key为字符串,所以无论首字母大小写均能取到
③若希望向Execute函数中传入多个参数,则可以把这些参数嵌套进一个map中来实现
user1 := UserInfo{Name: "Tom1", gender: "female1", Age: 181}
m1 := map[string]interface{} {
"Name": "Tom2",
"Gender": "female2",
"Age": 182,
"fuck": "Golang",
}
t.Execute(w, map[string]interface{} {
"m1" : m1,
"user1" : user1,
})
在 .tmpl 文件中注意,要先总主map中取值,再从各自的结构中取值
<!DOCTYPE html>
<html lang="zh-CN">
<head><title>hello, this is a title</title></head>
<body>
<p>名字:{{.m1.Name}}</p>
<p>年龄:{{.user1.Age}}</p>
<p>性别:{{.m1.Gender}}</p>
</body>
</html>
2.注释
{{/* a comment */}}
在执行时会忽略,可以直接换行
注释不能嵌套,并且必须紧贴分界符始止
3.pipeline
pipeline是指产生数据的操作。比如{{.}}、{{.Name}}等。Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。
注意:并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline。
4.声明变量
还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果
<p>{{ $age := .user1.Age }}</p>
<p>{{ $age }}</p>
其中$age是变量的名字,在后续的代码中就可以使用该变量了,但要注意:变量名是 $age ,不是 age 。要加dollar符号$
5.移除空格
有时在使用模板语法的时候会引入一些空格或换行符,此时可以使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号
<p>名字: {{- .m1.Name -}}</p>
<p>年龄:{{.user1.Age}}</p>
<p>性别: {{- .m1.Gender -}}</p>
注意:-要紧挨{{和}},同时 - 与模板值之间需要使用空格分隔。
6.条件判断
Go语言在.tmpl文件中的条件判断共有以下几种:
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
7.比较函数 - 搭配条件判断
布尔函数会将任何类型的零值视为假,其余视为真。
下面是定义为函数的二元比较运算的集合:
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”)。但是,整数和浮点数不能互相比较。
具体语法:
{{if lt .user1.Age 18}}
<p>好好学习!</p>
{{else}}
<p>好好工作</p>
{{end}}
注意 比较函数名写在前面,其后跟需要比较的两个变量
8.range函数 - 遍历
Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道
{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出
{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。
举例:
先在渲染模板这一步向前端传入一个切片r1
t.Execute(w, map[string]interface{} {
"m1" : m1,
"user1" : user1,
"r1" : []string{
"football",
"basketball",
"tennis",
},
})
在tmpl文件中使用range函数遍历打印
{{range $index, $values := .r1}}
<p>{{$index}} - {{$values}}</p>
{{end}}
结果:
0 - football
1 - basketball
2 - tennis
9.with - 声明作用域
with在tmpl文件中可以更改.的作用域,简化代码
{{with .m1}}
{{.Name}}
{{.Age}}
{{end}}
在with与end之间,.均被替换为了.m1。所以直接用一个点就可以访问到m1的字段
10.在模板中自定义函数
http.HandleFunc("/", f1)
①首先在要执行的 f1 函数中定义这个自定义函数
k := func(name string)(string, error) {
return name + "真帅!", nil
}
注意:Golang规定这里的自定义函数要么只有一个返回值,要么有一个返回值 + error(报错)
②在解析模板之前为模板添加上这个自定义函数
t := template.New("hello.tmpl") //创建模板对象(未解析、未渲染)
t.Funcs(template.FuncMap{
"func_kua": k, //最后在模板中使用 “func_kua” 作为自定义的函数名
})
注意:先利用New函数创建一个模板对象(未解析、未渲染),再使用Funcs函数通过特殊的语法把这个函数添加进去,起名字为 “func_kua”
③④解析模板、渲染模板
//解析模板
_, err := t.ParseFiles("./hello.tmpl") //解析模板对象 t
if err != nil {
fmt.Printf("template parse failed, err: %v", err)
return
}
//渲染模板
name := "Tom"
t.Execute(w, name) //渲染模板对象 t
⑤在tmpl文件中使用自定义函数
<body>
{{func_kua .}}
</body>
函数名 + 参数
11.嵌套template
- 可以在template中嵌套其他的template
- 这个template可以是单独的文件,也可以是通过
define定义的template
举例:hello.tmpl文件如下
<!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>tmpl test</title>
</head>
<body>
<h1>测试嵌套template语法</h1>
<hr>
{{template "ul.tmpl"}}
<hr>
{{template "ol.tmpl"}}
</body>
</html>
{{ define "ol.tmpl"}}
<ol>
<li>吃饭</li>
<li>睡觉</li>
<li>打豆豆</li>
</ol>
{{end}}
ul.tmpl文件内容如下:
<ul>
<li>注释</li>
<li>日志</li>
<li>测试</li>
</ul>
-
总共有三个tmpl模板文件,第一个为基础的hello.tmpl;第二个为在hello中定义的ul.tmpl;第三个为独立的ul.tmpl文件
-
由1.所述,只需要解析并渲染hello与ul这两个tmpl文件即可
http.HandleFunc("/tmplDemo", demo1)
func demo1(w http.ResponseWriter, r *http.Request) {
//解析模板
t, err := template.ParseFiles("./hello.tmpl", "./ul.tmpl")
if err != nil {
fmt.Printf("template parse failed, err: %v", err)
return
}
//渲染模板
name := "Tom"
t.Execute(w, name)
}
注意!!!ParseFiles函数在解析多个tmpl文件时,母模板文件必须在前、其子模板文件必须在后!!!!注意顺序
访问127.0.0.1:9000/tmplDemo
12.Block - 继承(Flask 中也有类似的block设定)
大多数网站都有若干相似的子网页,它们只有基本内容不同,其他比如导航栏、侧边栏等都是完全相同的。
在实际编程中要做到这点,不是每一个子网页的html(tmpl)文件都从头到尾自己编写,而是使用block继承功能,指定一个根模板 base.tmpl ,其余的子网页都由此模板继承而来。
这样,通过修改根模板的内容就可以对大量网页快速维护升级,而不是一个个修改。
①根模板 base.tmpl 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>模板继承</title>
<style>
* {
margin: 0;
}
.nav {
height: 50px;
width: 100%;
position: fixed;
top: 0;
background-color: burlywood;
}
.main {
margin-top: 50px;
}
.menu {
width: 20%;
height: 100%;
position: fixed;
left: 0;
background-color: cornflowerblue;
}
.center {
text-align: center;
}
</style>
</head>
<body>
<div class="nav"></div>
<div class="main">
<div class="menu"></div>
<div class="content center">
{{block "unfilled" .}}{{end}}
</div>
</div>
</body>
</html>
其中{{block "unfilled" .}}{{end}}即为可以被子模版替换的部分
- 这部分被命名为
unfilled - 在
unfilled之后还需要把.传入其中,供子模版使用
②子模版 index.tmpl 代码
{{/*1.继承根模板*/}}
{{template "base.tmpl"}}
{{/*2.重新定义块*/}}
{{define "unfilled"}}
<h1>这是index页面</h1>
<p>hello {{.}}</p>
{{end}}
③主函数中对子模版的解析和渲染
http.HandleFunc("/index_new", index_new)
func index_new(w http.ResponseWriter, r *http.Request) {
//解析模板
t, err := template.ParseFiles("./template/base.tmpl", "./template/index.tmpl")
if err != nil {
fmt.Printf("template parse failed, err: %v", err)
}
//渲染模板
name := "Tom"
t.Execute(w, name)
}
注意:因为子模版继承根模板,所以根、子模板都需要解析和渲染
③'主函数的另一种写法
func home_new(w http.ResponseWriter, r *http.Request) {
//解析模板
t, err := template.ParseFiles("./template/base.tmpl", "./template/home.tmpl")
if err != nil {
fmt.Printf("template parse failed, err: %v", err)
}
//渲染模板
name := "Tom"
t.ExecuteTemplate(w, "home.tmpl", name)
}
当主函数使用ExecuteTemplate方法只渲染子模版home时,在继承时就必须要把根模板中的参数一并继承下来,具体代码为:(在第一步的最后要有一个点 .)
{{/*1.继承根模板*/}}
{{template "base.tmpl" .}}
{{/*2.重新定义块*/}}
{{define "unfilled"}}
<h1>这是home页面</h1>
<p>hello {{.}}</p>
{{end}}
④补充
如果我们的模板名称冲突了,例如不同业务线下都定义了一个index.tmpl模板,我们可以通过下面两种方法来解决。
- 在模板文件开头使用
{{define 模板名}}语句显式地为模板命名。 - 可以把模板文件存放在
templates文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")解析模板。(正则匹配)
13.修改默认的标识符
Go标准库的模板引擎使用的花括号
{{和}}作为标识,而许多前端框架(如Vue和AngularJS)也使用{{和}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符:修改前端标识符或者修改Go语言标识符
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")
举例:希望使用 {[ . ]} 来标识变量
t, err := template.New("index.tmpl").Delims("{[", "]}").ParseFiles("./index.tmpl")
在tmpl模板文件中:
<body>
<div>Hello {[.]}</div>
</body>
二、text/template与html/tempalte的区别
html/template针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击如果用
text/template则不会对风险内容进行转义,那么如果用户提交的是一些html、JS语句,则均会直接在前端中执行,留下隐患
举例:定义下面的模板文件index.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>修改引擎的标识符</title>
</head>
<body>
<div>Hello <script>alert("你受到了来自用户的攻击!")</script></div>
</body>
</html>
渲染后的结果:
可见,如果某人把<script>alert("你受到了来自用户的攻击!")</script>输送到评论区,再展示到html中,那么就会在所有用户的网页上显示这一警告
(1)解决方法 - 使用http/template库
package main
import (
"fmt"
"html/template"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
//解析模板
t, _ := template.ParseFiles("index.tmpl")
//渲染模板
risk_code := "<script>alert(\"你受到了来自用户的攻击!\")</script>"
t.Execute(w, risk_code)
}
func main() {
//创建路由
http.HandleFunc("/index", index)
//监听是否有访问
err := http.ListenAndServe(":9000", nil)
if err != nil {
fmt.Printf("http server start failed, err: %v", err)
return
}
}
html/template库会对html、js代码完成转义工作,使得这些有风险的语句以字符串的形式显示在网页上
(2)手动决定转义与否 - 自定义safe函数
main.go
package main
import (
"fmt"
"html/template"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
//在解析模板之前传入自定义函数
t := template.New("index.tmpl")
t.Funcs(template.FuncMap{
"safe": func(s string) template.HTML {
return template.HTML(s) //把被转义了的s转化为html语句
},
})
//解析模板
t.ParseFiles("index.tmpl")
//渲染模板
risk_code := "<script>alert(\"你受到了来自用户的攻击!\")</script>"
safe_code := "<a href='http://liwenzhou.com'>liwenzhou的博客</a>"
t.Execute(w, map[string]string{
"riskCode" : risk_code,
"safeCode" : safe_code,
})
}
func main() {
//创建路由
http.HandleFunc("/index", index)
//监听是否有访问
err := http.ListenAndServe(":9000", nil)
if err != nil {
fmt.Printf("http server start failed, err: %v", err)
return
}
}
index.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>修改引擎的标识符</title>
</head>
<body>
<div>this is a safe code: {{.safeCode | safe}}</div>
<div>this is a risk code: {{.riskCode}}</div>
</body>
</html>
补充:管道符号 |
pipeline是指产生数据的操作。比如{{.}}、{{.Name}}等- Go的模板语法中支持使用管道符号
|链接多个命令:|前面的命令会将运算结果(或返回值)作为参数传递给后一个命令
所以在index.tmpl中,.safeCode通过管道符号|作为参数进入了safe函数,最后返回html语句,展示在界面上
结果:

浙公网安备 33010602011771号