html/template包实现了数据驱动的模板,用于生成可防止代码注入的安全的HTML内容。它提供了和text/template包相同的接口,Go语言中输出HTML的场景都应使用html/template`这个包。

一、模板与渲染

在一些前后端不分离的Web架构中,我们通常需要在后端将一些数据渲染到HTML文档中,从而实现动态的网页(网页的布局和样式大致一样,但展示的内容并不一样)效果。

我们这里说的模板可以理解为事先定义好的HTML文档文件,模板渲染的作用机制可以简单理解为文本替换操作–使用相应的数据去替换HTML文档中事先准备好的标记。

很多编程语言的Web框架中都使用各种模板引擎,比如Python语言中Flask框架中使用的jinja2模板引擎。

二、Go语言的模板引擎

Go语言内置了文本模板引擎text/template和用于HTML文档的html/template。它们的作用机制可以简单归纳如下:

  1. 模板文件通常定义为.tmpl.tpl为后缀(也可以使用其他的后缀),必须使用UTF8编码。

  2. 模板文件中使用{{}}包裹和标识需要传入的数据。

  3. 传给模板这样的数据就可以通过点号(.)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。

  4. {{}}包裹的内容外,其他内容均不做修改原样输出。

三、模板引擎的使用

Go语言模板引擎的使用可以分为三部分:定义模板文件、解析模板文件和模板渲染.

1. 定义模板文件

其中,定义模板文件时需要我们按照相关语法规则去编写,后文会详细介绍。

2. 解析模板文件

上面定义好了模板文件之后,可以使用下面的常用方法去解析模板文件,得到模板对象:

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的模板,然后对其调用上面的方法去解析模板字符串或模板文件。

3. 模板渲染

渲染模板简单来说就是使用数据去填充模板,当然实际上可能会复杂很多。

func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error

4. 基本示例

4.1 定义模板文件

我们按照Go模板语法定义一个hello.tmpl的模板文件,内容如下:

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Hello</title>
</head>
<body>
<p>Hello {{ . }}</p>
</body>
</html>

4.2 解析和渲染模板文件

然后我们创建一个main.go文件,在其中写下HTTP server端代码如下:

package main

import (
"html/template"
"log"
"net/http"
)

func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析模板
t,err := template.ParseFiles("./hello.tmpl")
if err!=nil {
log.Println("Parse template failed, err%v", err)
return
}
// 渲染模板
name := "飞哥哥"
err = t.Execute(w, name)
if err!=nil {
log.Println("render template failed, err%v", err)
return
}
}

func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9000", nil)
if err!=nil {
log.Println("http server start failed,err:%v",err)
}
}

将上面的main.go文件编译执行,然后使用浏览器访问http://127.0.0.1:9000就能看到页面上显示了“Hello 沙河小王子”。 这就是一个最简单的模板渲染的示例,Go语言模板引擎详细用法请往下阅读。

四、模板语法

1. {{.}}

模板语法都包含在{{}}中间,其中{{.}}中的点表示当前对象。当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

package main

import (
"html/template"
"log"
"net/http"
)

type User struct {
Name string
Gender string
Age int
}

func sayHello(w http.ResponseWriter, r *http.Request) {
// 解析模板
t,err := template.ParseFiles("./hello.tmpl")
if err!=nil {
log.Println("Parse template failed, err%v", err)
return
}
// 渲染模板
// 渲染字符串
name := "飞哥哥"
//err = t.Execute(w, name)
// 渲染结构体
user := User{
Name:name,
Gender:"男",
Age:23,
}
err = t.Execute(w, user)
if err!=nil {
log.Println("render template failed, err%v", err)
return
}
}

func main() {
http.HandleFunc("/", sayHello)
err := http.ListenAndServe(":9000", nil)
if err!=nil {
log.Println("http server start failed,err:%v",err)
}
}

模板文件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>Hello</title>
</head>
<body>
<p>Hello {{.Name}}</p>
<p>性别:{{.Gender}}</p>
<p>年龄:{{.Name}}</p>
</body>
</html>  

渲染map类型

func sayHello(w http.ResponseWriter, r *http.Request)  {
// 解析模板
t,err := template.ParseFiles("./hello.tmpl")
if err!=nil {
log.Println("Parse template failed, err%v", err)
return
}
// 渲染模板
// 渲染字符串
name := "飞哥哥"
//err = t.Execute(w, name)
// 渲染结构体
user := User{
Name:name,
Gender:"男",
Age:23,
}
//err = t.Execute(w, user)
// 渲染map
m := map[string]interface{}{
"name": name,
"gender": "男",
"age": 24,
}
//err = t.Execute(w, m)
hobbyList := []string{
"篮球",
"足球",
"双色球",
}

err = t.Execute(w, map[string]interface{}{
"m": m,
"user": user,
"hobby": hobbyList,
})
if err!=nil {
log.Println("render template failed, err%v", err)
return
}
}

hello.tmpl

<p>Hello {{ .user.Name }}</p>
<p>年龄 {{ .user.Age }}</p>
<p>性别 {{ .user.Gender }}</p>

<p>Hello {{ .m.name }}</p>
<p>年龄 {{ .m.age }}</p>
<p>性别 {{ .m.gender }}</p>

<hr>
{{ range $idx, $hobby := .hobby}}
<p>{{$idx}} - {{$hobby}}</p>

2. 注释

{{/* a comment */}}
注释,执行时会忽略。可以多行。注释不能嵌套,并且必须紧贴分界符始止。

3. pipeline

  pipeline是指产生数据的操作。比如{{.}}{{.Name}}等。Go的模板语法中支持使用管道符号|链接多个命令,用法和unix下的管道类似:|前面的命令会将运算结果(或返回值)传递给后一个命令的最后一个位置。

注意:并不是只有使用了|才是pipeline。Go的模板语法中,pipeline的概念是传递数据,只要能产生数据的,都是pipeline

4. 变量

我们还可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果。具体语法如下:

$obj := {{.}}

其中$obj是变量的名字,在后续的代码中就可以使用该变量了。

5. 移除空格

有时候我们在使用模板语法的时候会不可避免的引入一下空格或者换行符,这样模板最终渲染出来的内容可能就和我们想的不一样,这个时候可以使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。

例如:

{{- .Name -}}

注意:-要紧挨{{}},同时与模板值之间需要使用空格分隔。

6. 条件判断

Go模板语法中的条件判断有以下几种:

{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

7. range

Go的模板语法中使用range关键字进行遍历,有以下两种写法,其中pipeline的值必须是数组、切片、字典或者通道。

{{range pipeline}} T1 {{end}}
如果pipeline的值其长度为0,不会有任何输出

{{range pipeline}} T1 {{else}} T0 {{end}}
如果pipeline的值其长度为0,则会执行T0。

8. with

{{with pipeline}} T1 {{end}}
如果pipeline为empty不产生输出,否则将dot设为pipeline的值并执行T1。不修改外面的dot。

{{with pipeline}} T1 {{else}} T0 {{end}}
如果pipeline为empty,不改变dot并执行T0,否则dot设为pipeline的值并执行T1。

9. 预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用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。
这个函数在html/template中不可用。
urlquery
以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
这个函数在html/template中不可用。
js
返回与其参数的文本表示形式等效的转义JavaScript。
call
执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
其中Y是函数类型的字段或者字典的值,或者其他类似情况;
call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

10. 比较函数

布尔函数会将任何类型的零值视为假,其余视为真。

下面是定义为函数的二元比较运算的集合:

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”)。但是,整数和浮点数不能互相比较。

11. 自定义函数

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

12. 嵌套template

我们可以在template中嵌套其他的template。这个template可以是单独的文件,也可以是通过define定义的template。

举个例子: t.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>

我们注册一个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后进行解析。

13. 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模板,我们可以通过下面两种方法来解决。

  1. 在模板文件开头使用{{define 模板名}}语句显式的为模板命名。

  2. 可以把模板文件存放在templates文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")解析模板。

14. 修改默认的标识符

Go标准库的模板引擎使用的花括号{{}}作为标识,而许多前端框架(如VueAngularJS)也使用{{}}作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:

template.New("index3.tmpl").Delims("{[","]}").ParseFiles("templates/index3.tmpl")
<h1>hello {[ . ]}</h1>

五、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函数就可以了。

Go标准库之html/template的更多相关文章

  1. Go标准库:Go template用法详解

    本文只介绍template的语法和用法,关于template包的函数.方法.template的结构和原理,见:深入剖析Go template. 入门示例 以下为test.html文件的内容,里面使用了 ...

  2. python学习笔记系列----(八)python常用的标准库

    终于学到了python手册的最后一部分:常用标准库.这部分内容主要就是介绍了一些基础的常用的基础库,可以大概了解下,在以后真正使用的时候也能想起来再拿出来用. 8.1 操作系统接口模块:OS OS模块 ...

  3. 《C++ Primer》学习笔记【第二部分 C++标准库】

    第8章 IO库 IO对象不能复制,即1.IO对象不能存储在vector或其他容器中   2.如果需要传递或返回IO对象,必须传递或返回指向该对象的指针或引用. 一般情况下,如果要传递IO对象以便对它进 ...

  4. 【转】C++标准库和标准模板库

    C++强大的功能来源于其丰富的类库及库函数资源.C++标准库的内容总共在50个标准头文件中定义.在C++开发中,要尽可能地利用标准库完成.这样做的直接好处包括:(1)成本:已经作为标准提供,何苦再花费 ...

  5. C++标准库简介、与STL的关系。

    转自http://www.cnblogs.com/xiongjiaji/archive/2011/06/22/2476490.html C++标准库的所有头文件都没有扩展名.C++标准库的内容总共在5 ...

  6. C++ 标准库类型-String,Vector and Bitset

    <C++ Primer 4th>读书摘要 最重要的标准库类型是 string 和 vector,它们分别定义了大小可变的字符串和集合.这些标准库类型是语言组成部分中更基本的那些数据类型(如 ...

  7. c++标准库和stl关系

    C++标准库的所有头文件都没有扩展名.C++标准库的内容总共在50个标准头文件中定义,其中18个提供了C库的功能. <cname>形式的标准头文件[ <complex>例外]其 ...

  8. STL笔记(6)标准库:标准库中的排序算法

    STL笔记(6)标准库:标准库中的排序算法 标准库:标准库中的排序算法The Standard Librarian: Sorting in the Standard Library Matthew A ...

  9. 什么是C++标准库?

    C++中的标准程序库(简称标准库)是类库和函数的集合,其使用核心语言写成.标准程序库提供若干泛型容器.函数对象.泛型字符串和流(包含交互和文件I/O),支持部分语言特性和常用的函数,如开平方根.C++ ...

随机推荐

  1. CPU中的上下文

    目录 一.简介 二.进程切换 三.线程切换 四.中断切换 五.中断检测和查看 六.模拟 一.简介 Linux是多任务操作系统,cpu划分固定时间片,分给每个进程,当前进程时间片执行完毕,将挂起,运行下 ...

  2. for循环中的变量泄漏

    经典的案例 let arr = [] for(var i =0;i<=5;i++){ arr[i]= function fn(){ console.log(i) } } arr[0]() //6 ...

  3. 在【自定义列】中使用M函数(Power Query 之 M 语言)

    数据源: "品名"一列 目标: 提取品名中的首字符,生成新列:"品名简称" 解决方案: 在[自定义列]中使用M函数Text.Start 步骤: 打开[自定义列] ...

  4. CF918B Radio Station 题解

    Content 有 \(n\) 个形如 \(a_i.b_i.c_i.d_i\) 的 IP 地址.有 \(m\) 条命令,每条命令由一条字符串 \(s\) 和一个形如 \(p.q.r.s\) 的 IP ...

  5. linux 下查看文件修改时间

    linux 下查看文件修改时间 等 http://blog.sina.com.cn/s/blog_6285b04e0100f4xr.html 查看文件时间戳命令:stat awk.txtFile: ` ...

  6. mysql 禁止自动提交设置

    mysql禁止自动提交的设置, 在my.ini文件里加上如下的一句便可 init_connect='SET autocommit=0' 但是有个问题,对root用户进行autocommit变量的查询, ...

  7. 访问struts2的action页面出现白板问题

    访问struts2的action页面出现白板问题 故需要设置拦截此action的拦截栈, <bean id="authenticationInterceptor" class ...

  8. 最强最全面的大数据SQL经典面试题(由31位大佬共同协作完成)

    本套SQL题的答案是由许多小伙伴共同贡献的,1+1的力量是远远大于2的,有不少题目都采用了非常巧妙的解法,也有不少题目有多种解法.本套大数据SQL题不仅题目丰富多样,答案更是精彩绝伦! 注:以下参考答 ...

  9. JAVAWEB项目处理XSS漏洞攻击处理方案

    对页面传入的参数值进行过滤,过滤方法如下 public static String xssEncode(String s) { if (s == null || s.equals("&quo ...

  10. Android 崩溃错误

    SIGSEGV ---段错误. 遇到此错误的可能情况是: 1.缓冲区溢出---通常由指针引用超出范围引起. 2.堆栈溢出---请记住默认堆栈大小为8192K. 3.我们的判断系统禁止文件访问---文件 ...