Golang笔记-Typora2

Go语言标准库 - template

一、模板与渲染

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

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

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

简单地说,渲染就是把后端变量中的数据显示在前端的html页面中

(1)模板引擎的使用

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

  1. 模板文件通常定义为.tmpl.tpl为后缀(也可以使用其他的后缀),必须使用UTF8编码。
  2. 模板文件中使用{{}}包裹和标识需要传入的数据。
  3. 传给模板这样的数据就可以通过点号(.)来访问,如果数据是复杂类型的数据,可以通过{ { .FieldName }}来访问它的字段。
  4. {{}}包裹的内容外,其他内容均不做修改原样输出。

(与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>
  1. 总共有三个tmpl模板文件,第一个为基础的hello.tmpl;第二个为在hello中定义的ul.tmpl;第三个为独立的ul.tmpl文件

  2. 由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

image-20210725151636131

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

  1. 在模板文件开头使用{{define 模板名}}语句显式地为模板命名。
  2. 可以把模板文件存放在templates文件夹下面的不同目录中,然后使用template.ParseGlob("templates/**/*.tmpl")解析模板。(正则匹配)

13.修改默认的标识符

Go标准库的模板引擎使用的花括号{{}}作为标识,而许多前端框架(如VueAngularJS)也使用{{}}作为标识符,所以当我们同时使用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>

渲染后的结果:

image-20210725172147952

可见,如果某人把<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语句,展示在界面上

结果:

image-20210725174636499

posted @ 2021-07-25 17:52  vosoland  阅读(166)  评论(0)    收藏  举报