文章目录
模板引擎的使用
在 Go 语言中使用 template
包来进行模板处理,使用类似 Parse()
、ParseFile()
、Execute()
等方法从文件或者字符串加载模板,然后执行模板的 merge 操作。模板引擎的使用可以分为三个阶段:定义模板文件、解析模板文件和模板渲染。
- 定义模板文件。
定义模板文件时需要按照相关语法规则去编写,也可以使用New(name string)
函数创建一个名为 name
的模板,然后对其调用相关方法去解析模板字符串或模板文件,其函数声明如下:
// 创建指定模板名称的模板对象
func New(name string) *Template
- 解析模板文件。
定义好模板文件后可以使用如下常用的方法或函数去解析模板文件来得到模板对象。
// 解析模板内容
func (t *Template) Parse(src string) (*Template, error)
// 解析模板文件
func ParseFiles(filenames ...string) (*Template, error)
// 正则匹配解析文件
func ParseGlob(pattern string) (*Template, error)
- 模板渲染。
渲染模板可以使用如下的方法让数据去填充模板。
func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
例如编写一个如下的程序展示关于对
hello.tmpl
模板文件进行渲染的用法。
- 定义模板文件。
按照 Go 模板语法定义创建一个 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>Go Web</title>
</head>
<body>
<p>Welcome to {{.}}!</p>
</body>
</html>
- 解析和渲染模板文件。
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"net/http"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./hello.tmpl")
if err != nil {
fmt.Println("Creating the template file is failed: %v", err)
return
}
// 利用给定数据渲染模板,并将结果写入w,执行模板的merge操作
tmpl.Execute(w, "CQUPT")
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
Welcome to CQUPT!
通过这个例子可以发现 Go 语言的模板操作和其他语言的模板处理类似,都是先获取数据,然后渲染数据。
模板语法
字段操作
Go 语言的模板语法都包含在 {{
和 }}
中间,模板通过双大括号来包含需要在渲染时被替换的字段,{{.}}
中的点表示当前对象,如果要访问当前对象的字段,可以通过 {{.FieldName}}
方式,这个字段必须是导出的(字段首字母必须大写),否则在渲染的时候就会报错,但如果调用了一个不存在的字段不会报错的,而是输出为空。
注意:模板中输出
{{.}}
一般用于字符串对象,默认会调用fmt
包输出字符串的内容。
例如按照 Go 模板语法定义创建一个 hello.tmpl
的模板文件,该文件的具体内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go Web</title>
</head>
<body>
<p>加油 {{ . }} 同学!</p>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"fmt"
"html/template"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
// 解析模板
t, err := template.ParseFiles("./hello.tmpl")
/**
// 等价于如下代码部分
t := template.New("hello.html")
hello := `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go Web</title>
</head>
<body>
<p>加油 {{ . }} 同学!</p>
</body>
</html>`
t, _ := t.Parse(hello)
*/
if err != nil {
fmt.Println("Parsing the template file failed: %v", err)
return
}
// 渲染模板
name := "cqupthao"
t.Execute(w, name)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
加油 cqupthao 同学!
当传入一个结构体对象时,可以通过 .
来访问结构体的对应字段,例如按照 Go 模板语法定义创建一个 class.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>Go Web</title>
</head>
<body>
<p>姓名:{{.Name}}</p>
<p>性别:{{.Gender}}</p>
<p>年龄:{{.Age}}</p>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"fmt"
"html/template"
"net/http"
"os"
)
//定义用户结构体
type UserInfo struct {
Name string
Gender string
Age int
}
func process(w http.ResponseWriter, r *http.Request) {
// 获取项目的绝对路径
wd, err := os.Getwd()
if err != nil {
fmt.Printf("Getting way is failed: %v", err)
return
}
fmt.Println("class.tml's way is:", wd)
// 解析指定文件生成模板对象
tmpl, err := template.ParseFiles("./class.tmpl")
if err != nil {
fmt.Println("Creating the template file ifs failed: %v", err)
return
}
// 利用给定数据渲染模板,并将结果写入 w
user := UserInfo{
Name: "cqupthao",
Gender: "男",
Age: 23,
}
tmpl.Execute(w, user)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
姓名:cqupthao
性别:男
年龄:23
运行程序访问网址后同时输出 class.tml's way is: /home/programs/template
,由程序运行的结果可知,在模板文件内{{.}}
代表了当前变量(即在非循环体内,{{.}}
就代表传入的那个变量)。
在 Go 语言中模板使用 {{/* a comment */}}
进行注释,可多行,但注释不能嵌套并且必须紧贴分界符始止。注释后的内容不会被引擎进行替换,但需要注意的是注释行在替换的时候也会占用行,所以应该去除前缀和后缀空白,否则会多一空行,具体的使用如下:
{{- /* a comment without prefix/suffix space */}}
{{/* a comment without prefix/suffix space */ -}}
{{- /* a comment without prefix/suffix space */ -}}
注意:仅去除前缀或后缀空白,不要同时都去除,否则会破坏原有的格式。
当传入的变量是 map 形式时也可以在模板文件中通过 .
根据 key 来取值,例如如下程序形式:
// 采用一个 map 储存
m := map[string]interface{}{
"Name": "cqupthao",
"Age": 23,
"Gender": "男",
}
例如编写一个 HTTP server 端程序解析该 class.tmpl
模板文件进行渲染,该程序的具体内容如下:
package main
import (
"fmt"
"html/template"
"net/http"
)
func process(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("./class.tmpl")
if err != nil {
fmt.Println("Creating the template file is failed: %v", err)
return
}
// 采用一个 map 储存
m := map[string]interface{}{
"Name": "Tao",
"Age": 22,
"Gender": "男",
}
tmpl.Execute(w,m)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
姓名:Tao
性别:男
年龄:22
如果要把 map 和结构体的内容都传递到前端,就需要在定义一个大的 map 结构来进行存储,例如如下的程序形式:
// 采用结构体储存
u := UserInfo{
Name: "cqupthao",
Gender: "男",
Age: 23,
}
// 采用一个 map 储存
m1 := map[string]interface{}{
"Name": "Tao",
"Age": 22,
"Gender": "男",
}
// 采用 map 和结构体储存
m2 := map[string]interface{}{
"map": m1,
"user": u,
}
上面的展示了如何针对一个对象的字段输出,如果字段里面还有对象,那么在 Go 语言的模板语法中可以使用 range
或 with
关键字进行遍历,其中 pipeline
的值必须是数组、切片、字典或者通道。
- range
// 如果 pipeline 的值其长度为 0,不会有任何输出
{{range pipeline}} T1(Dot is set to the element {{ . }}) {{end}}
// 如果 pipeline 的值其长度为 0,则会执行 T0
{{range pipeline}} T1 {{else}} T0 {{end}}
- with
// 如果 pipeline 为 empty 不产生输出,否则将 dot 设为 pipeline 的值并执行 T1 ,不修改外面的 dot
{{with pipeline}} T1(Dot is set to pipeline) {{end}}
// 如果 pipeline 为 empty ,不改变 dot 并执行 T0 ,否则 dot 设为 pipeline 的值并执行 T1
{{with pipeline}} T1 {{else}} T0 {{end}}
{{range}}
和 Go 语言语法里面的 range 类似,循环操作数据。
例如按照 Go 模板语法定义创建一个 range.html
的模板文件,该文件的具体内容如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
<div>
{{ range . }}
<div>{{ . }}</div>
{{ else }}
<div>Nothing to show !</div>
{{ end }}
</div>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"net/http"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("range.html")
if err != nil {
fmt.Println("Opening the template file is failed: %v \n", err)
return
}
WeekDays := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
t.Execute(w, WeekDays)
t.Execute(w, nil)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Nothing to show !
{{with}}
操作是指当前对象的值,指定范围内为点设置值,类似上下文的概念。
例如按照 Go 模板语法定义创建一个 with.html
的模板文件,该文件的具体内容如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
<div>The dot is {{ . }}</div>
<div>
{{ with "world"}}
Now the dot is set to {{ . }}
{{ end }}
{{ with ""}}
Now the dot is set to {{ . }}
{{ else }}
The dot is still {{ . }}
{{ end }}
</div>
<div>The final dot is {{ . }} !</div>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"net/http"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("with.html")
if err != nil {
fmt.Println("Opening the template file is failed: %v \n", err)
return
}
t.Execute(w, "hello")
t.Execute(w, "world")
t.Execute(w, "xxx")
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
The dot is hello
Now the dot is set to world The dot is still hello
The final dot is hello !
The dot is world
Now the dot is set to world The dot is still world
The final dot is world !
The dot is xxx
Now the dot is set to world The dot is still xxx
The final dot is xxx !
条件判断
Go模板语法中的条件判断有以下几种:
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
在 Go 语言模板里进行条件判断和 Go 语言的 if-else
语法类似的方式来处理,如果 pipeline 为空,那么 if
就认为是 false
。
例如按照 Go 模板语法定义创建一个 if.html
的模板文件,该文件的具体内容如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
{{ if . }}
<div>The number is greater than 2!</div>
{{ else }}
<div>The number is 2 or less!</div>
{{ end }}
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"math/rand"
"net/http"
"time"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("if.html")
if err != nil {
fmt.Println("Opening the template file is failed: %v \n", err)
return
}
rand.Seed(time.Now().Unix())
t.Execute(w, rand.Intn(9) > 2)
t.Execute(w, rand.Intn(6) > 2)
t.Execute(w, rand.Intn(3) > 2)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
The number is 2 or less !
The number is greater than 2 !
The number is 2 or less !
注:if 里面无法使用条件判断,例如
Mail=="@gmail.com"
,这样的判断是不正确的,if 里面只能是 bool 值。
pipeline
pipeline
是指产生数据的操作,比如{{.}}
、{{.Name}}
等,Go 语言的模板语法中支持使用管道符号 |
连接多个命令,用法和 UNIX 下的管道类似(|
前面的命令会将运算结果或返回值传递给后一个命令的最后一个位置),例如:
{{.}} | printf "%s\n" "xxx"
{{.}}
的结果将传递给 printf,且传递的参数位置是 “xxx” 之后。
命令可以有超过 1 个的返回值,这时第二个返回值必须为 err 类型,例如下面的 (len "output")
是 pipeline :
{{println (len "output")}}
下面是 pipeline 的几种示例,它们都输出 "output"
:
{{`"output"`}}
{{printf "%q" "output"}}
{{"output" | printf "%q"}}
{{printf "%q" (print "out" "put")}}
{{"put" | printf "%s%s" "out" | printf "%q"}}
{{"output" | printf "%s" | printf "%q"}}
注意:并不是只有使用了
|
才是 pipeline ,Go 语言的模板语法中,pipeline
的概念是传递数据,只要能产生数据的,都是pipeline
。
Go 语言模板支持 pipe 数据,在 Go 语言里面任何 {{}}
里的都是 pipeline
数据,可以采用如下方式把输出全部转化 HTML 的实体,调用其他的函数也是类似的方式。
{{. | html}}
pipeline 为 false 时对应各种数据对象的零值:数是 0 ,指针或接口是 nil ,数组、slice、map 或 string 则是 0 。
变量
在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果,变量的声明如下:
// 未定义过的变量
$var := pipeline
// 已定义过的变量
$var = pipeline
例如编写一个关于变量的定义与使用的程序,该程序的具体内容如下:
package main
import (
"os"
"text/template"
)
func main(){
tx := template.Must(template.New("hh").Parse(
`{{range $x := . -}}
{{$y := 333}}
{{- if (gt $x 33)}}{{println $x $y ($z := 444)}}{{- end}}
{{- end}}
`))
s := []int{11, 22, 33, 44, 55}
_ = tx.Execute(os.Stdout, s)
}
程序输出的结果如下:
44 333 444
55 333 444
上面的示例中,使用 range 迭代 slice ,每个元素都被赋值给变量 $x
,每次迭代过程中,都新设置一个变量 $y
,在内层嵌套的 if 结构中,可以使用这个两个外层的变量。
在 if 的条件表达式中,使用了一个内置的比较函数 gt ,如果 $x
大于 33 ,则为 true ,在 println 的参数中还定义了一个 $z
,之所以能定义是因为 ($z := 444)
的过程是一个 pipeline ,可以先运行。
-
变量有作用域,只要出现 end ,则当前层次的作用域结束,内层可以访问外层变量,但外层不能访问内层变量。
-
有一个特殊变量
$
,它代表模板的最顶级作用域对象(通俗地理解,是以模板为全局作用域的全局变量),在Execute()
执行的时候进行赋值且一直不变。 -
变量不可在模板之间继承。
对于特殊变量 .
和 $
,例如编写如下程序,该程序的具体内容如下:
package main
import (
"os"
"text/template"
)
func main() {
t1 := template.New("test1")
tmpl, _ := t1.Parse(
`{{- define "T1"}}ONE {{println .}}{{end}}
{{- define "T2"}}{{template "T1" $}}{{end}}
{{- template "T2" . -}}
`)
_ = tmpl.Execute(os.Stdout, "CQUPT")
}
程序输出的结果如下:
ONE CQUPT
该程序使用 define
额外定义了 T1 和 T2 两个模板,T2 中嵌套了 T1 ,{{template "T2" .}}
的点代表顶级作用域的 CQUPT 对象。在 T2 中使用了特殊变量 $
,这个 $
的范围是 T2 的,不会继承顶级作用域 CQUPT ,但因为执行 T2 的时候,传递的是 .
,所以这里的 $
的值仍是 CQUPT 。
不仅 $
不会在模板之间继承,.
也不会在模板之间继承(其它所有变量都不会继承),实际上,template 可以看作是一个函数,它的执行过程是 template("T2" . )
,如果把上面的 $
换成 .
,结果是一样的;如果换成 {{template "T2"}}
,则 $=nil
。
在一些操作中申明局部变量,如
with range if
过程中申明局部变量,这个变量的作用域是 {{end}}
之前,申明局部变量的方式如下:
{{ range $key, $value := . }}
The key is {{ $key }} and the value is {{ $value }}
{{ end }}
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
移除空格
template
引擎在进行替换时是完全按照文本格式进行替换的,除了需要评估和替换的地方,所有的行分隔符、空格等等空白都原样保留,对于要解析的内容,不要随意缩进、随意换行。
在使用模板语法时会不可避免的引入一下空格或者换行符,可以在 {{
符号的后面加上短横线并保留一个或多个空格 "- " 来去除它前面的空白(包括换行符、制表符、空格等);在 }}
的前面加上一个或多个空格以及一个短横线 “-” 来去除它后面的空白 ,例如如下的示例:
{{23}} < {{45}} -> 23 < 45
{{23}} < {{- 45}} -> 23 <45
{{23 -}} < {{45}} -> 23< 45
{{23 -}} < {{- 45}} -> 23<45
注意:
-
要紧挨着{{
和}}
,同时与模板值之间需要使用空格分隔。
修改默认的标识符
Go 标准库的模板引擎使用的花括号 {{
和 }}
作为标识,许多前端框架(如 Vue
和 AngularJS
)也使用 {{
和 }}
作为标识符,当同时使用 Go 语言模板引擎和以上前端框架时就会出现冲突,这个时候需要修改标识符,修改 Go 语言模板引擎默认的标识符方式如下:
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")
模板函数
预定义函数
执行模板时函数从两个函数字典中查找,首先是模板函数字典,然后是全局函数字典,一般不在模板内定义函数,而是调用 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 ,模板执行会中断并返回给调用模板执行者该错误;
比较函数
布尔函数会将任何类型的零值视为假,其余视为真,下面是定义为函数的二元比较运算的集合:
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 语言的模板支持自定义函数,自定义函数通过调用 Funcs()
方法来实现,该方法的声明如下:
func (t *Template) Funcs(funcMap FuncMap) *Template
Funcs()
方法会向模板对象的函数字典加入参数 funcMap 内的键值对,如果 funcMap 某个值不是函数类型或该函数类型不符合要求,则会报 panic
错误,但可以对模板对象的函数列表的成员进行重写,该方法返回模板对象以便进行链式调用,FuncMap 类型的定义如下:
type FuncMap map[string]interface{}
FuncMap 类型定义了函数名字符串到函数的映射,每个函数都必须有 1 个或 2 个返回值,第 2 个为必须是 error 接口类型,若返回的 error 值非 nil
,则模板执行会中断并返回该错误给调用者。
自定义模板函数过程如下:
(1)创建 FuncMap 映射,键为函数名(自定义字符串),值为实际定义的函数。
(2)调用 Funcs()
方法将 FuncMap 与模板进行绑定。
例如按照 Go 模板语法定义创建一个 data.tmpl
的模板文件,该文件的具体内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
<div>The date/time is {{ . }}</div>
<div>The date/time is {{ . | getdate}}</div>
<div>The date/time is {{ getdate . }}</div>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"net/http"
"time"
"fmt"
)
func FormatDate(t time.Time) string {
layout := "2022-02-02"
return t.Format(layout)
}
func process(w http.ResponseWriter, r *http.Request) {
funcMap := template.FuncMap{"getdate": FormatDate}
t := template.New("date.tmpl").Funcs(funcMap)
t, err := t.ParseFiles("date.tmpl")
if err != nil {
fmt.Println("Opening the template file is failed: %v \n", err)
return
}
t.Execute(w, time.Now())
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
The date/time is 2022-02-27 15:28:45.44440441 +0800 CST m=+3.140668588
The date/time is 272727-27-27
The date/time is 272727-27-27
又例如按照 Go 模板语法定义创建一个 funcs.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>Go Web</title>
</head>
<body>
<h1>{{loveGo}}</h1>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
)
func Welcome() string { //没参数
return "Welcome"
}
func Doing(name string) string { //有参数
return name + "! Let's begin to learn Go Web template !"
}
func process(w http.ResponseWriter, r *http.Request) {
htmlByte, err := ioutil.ReadFile("./funcs.tmpl")
if err != nil {
fmt.Println("Reading the html is failed: %v", err)
return
}
// 自定义一个匿名模板函数
loveGo := func() (string) {
return "欢迎一起学习《Go Web 编程》"
}
// 采用链式操作在 Parse() 方法之前调用 Funcs 添加自定义的loveGo函数
tmpl1, err := template.New("funcs").Funcs(template.FuncMap{"loveGo": loveGo}).Parse(string(htmlByte))
if err != nil {
fmt.Println("Creating the template file is failed: %v", err)
return
}
funcMap := template.FuncMap{
//在 FuncMap 中声明相应要使用的函数,然后就能够在template字符串中使用该函数
"Welcome": Welcome,
"Doing": Doing,
}
name := "cqupthao 同学"
tmpl2, err := template.New("test").Funcs(funcMap).Parse("{{Welcome}}\n{{Doing .}}\n")
if err != nil {
panic(err)
}
// 使用 user 渲染模板,并将结果写入 w
tmpl1.Execute(w, name)
tmpl2.Execute(w, name)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
欢迎一起学习《Go Web 编程》
Welcome cqupthao 同学! Let's begin to learn Go Web template !
Must() 函数
模板包里 Must()
函数的作用是检测模板是否正确,例如大括号是否匹配、注释是否正确关闭、变量是否正确书写。
例如编写一个程序使用 Must()
函数来判断模板是否正确,该程序的具体内容如下:
package main
import (
"fmt"
"text/template"
)
func main() {
tmpl := template.New("first")
template.Must(tmpl.Parse(" some static text /* and a comment */"))
fmt.Println("The first one parsed OK.")
template.Must(template.New("second").Parse("some static text {{ .Name }}"))
fmt.Println("The second one parsed OK.")
fmt.Println("The next one ought to fail.")
tErr := template.New("check parse error with Must")
template.Must(tErr.Parse(" some static text {{ .Name }"))
}
执行程序输出的结果如下:
The first one parsed OK.
The second one parsed OK.
The next one ought to fail.
panic: template: check parse error with Must:1: unexpected "}" in operand
goroutine 1 [running]:
text/template.Must(...)
/usr/local/go/src/text/template/helper.go:26
main.main()
/home/programs/template/must.go:15 +0x3fc
exit status 2
模板嵌套
模板嵌套 template
模板嵌套可以通过文件嵌套和 define
定义,即可以在 template 中嵌套其它的 template ,这个 template 可以是单独的文件,也可以是通过 define
定义的 template ,定义的格式如下:
{{define "name"}} T {{end}}
{{template "name"}}
{{template "name" pipeline}}
{{block "name" pipeline}} T {{end}}
//等价于
{{define "name"}} T {{end}}
{{template "name" pipeline}}
例如按照 Go 模板语法定义创建一个 t.tmpl
和一个 ul.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>Go Web</title>
</head>
<body>
<h1>测试嵌套template语法</h1>
<hr>
{{template "ul.tmpl"}}
<hr>
{{template "ol.tmpl"}}
<p>{{ .Name }},{{ .Age }}</p>
</body>
</html>
{{ define "ol.tmpl"}}
<p>姓名和年龄:</p>
{{end}}
<p>注释</p>
<p>日志</p>
<p>测试</p>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"fmt"
"html/template"
"net/http"
)
func tmplSample(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("t.tmpl", "ul.tmpl")
if err != nil {
fmt.Println("Creating the template is failed: %v", err)
return
}
user := UserInfo{
Name: "cqupthao",
Gender: "男",
Age: 23,
}
tmpl.Execute(w, user)
}
func main() {
http.HandleFunc("/", tmplSample)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
测试嵌套template语法
注释
日志
测试
姓名和年龄:
cqupthao,23
注:在解析模板时,被嵌套的模板一定要在后面解析,例如上面的示例中
t.tmpl
模板中嵌套了ul.tmpl
,所以ul.tmpl
要在t.tmpl
后进行解析。
block
block
是 定义模板 {{define "name"}} T1 {{end}}
和执行 {{template "name" pipeline}}
缩写,典型的用法是定义一组根模板,然后通过在其中重新定义块模板进行自定义,定义的格式如下:
{{block "name" pipeline}} T1 {{end}}
block 的第一个动作是执行名为 name 的模板,如果不存在,则在此处自动定义这个模板,并执行这个临时定义的模板,block 可以认为是设置一个默认模板,例如如下的定义:
{{block "T1" .}} one {{end}}
它首先表示 {{template "T1"}}
,也就是说先找到 T1 模板,如果 T1 存在,则执行找到的 T1 ;如果没找到 T1 ,则临时定义一个 {{define "T1"}} one {{end}}
并执行它。
例如按照 Go 模板语法定义一个根模板 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>
创建一个index.tmpl
模板文件 ”继承” base.tmpl
,该文件的具体内容如下:
{{template "base.tmpl"}}
{{define "content"}}
<div>Hello CQUPT!</div>
{{end}}
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"fmt"
"html/template"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request){
tmpl, err := template.ParseFiles("base.tmpl","index.tmpl")
if err != nil {
fmt.Println("Creating the template is failed: %v", err)
return
}
err = tmpl.ExecuteTemplate(w, "index.tmpl", nil)
if err != nil {
fmt.Println("Rendering the template is failed: %v", err)
return
}
}
func main() {
http.HandleFunc("/", index)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,浏览到页面内容如下:
Hello CQUPT!
如果定义的模板名称冲突了,例如不同业务线下都定义了一个index.tmpl
模板,可以通过下面两种方法来解决:
- 在模板文件开头使用
{{define 模板名}}
语句显式的为模板命名。 - 把模板文件存放在
xxx
文件夹下面的不同目录中,然后调用template.ParseGlob("xxx/**/*.tmpl")
方法解析模板。
例如按照 Go 模板语法定义创建两个不同模板文件 red.html
和 blue.html
定义同名模板和一个 layout.html
文件,文件具体内容如下:
// red.html
{{ define "content" }}
<h1 style="color: red;">Hello CQUPT!</h1>
{{ end }}
// blue.html
{{ define "content" }}
<h1 style="color: blue;">Hello CQUPT!</h1>
{{ end }}
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"math/rand"
"net/http"
"time"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
rand.Seed(time.Now().Unix())
var t *template.Template
if rand.Intn(10) > 5 {
t, _ = template.ParseFiles("layout.html", "red.html")
} else {
t, _ = template.ParseFiles("layout.html", "blue.html")
}
t.ExecuteTemplate(w, "layout", "")
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,刷新页面会显示不同的颜色,浏览到页面内容如下:
Hello CQUPT!
上下文感知
模板通过上下文感知(context-aware)的方式显示内容,根据内容所处的上下文改变其显示的内容。上下文感知具体指的是根据所处环境 css , json , html , url的 path ,url 的 query,自动进行不同格式的转义。
例如按照 Go 模板语法定义创建一个 context.tmpl
的模板文件,该文件的具体内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
<div>{{ . }}</div>
<div><a href="/{{ . }}">Path</a></div>
<div><a href="/?q={{ . }}">Query</a></div>
<div><a onclick="f('{{ . }}')">Onclick</a></div>
</body>
</html>
编写一个 HTTP server 端程序解析该模板文件进行渲染,该程序的具体内容如下:
package main
import (
"html/template"
"net/http"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("context.tmpl")
content := `I asked: <i>"What's up?"</i>`
t.Execute(w, content)
}
func main() {
http.HandleFunc("/", process)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后输入命令 `curl -i 127.0.0.1:8080/ 输出的结果如下:
HTTP/1.1 200 OK
Date: Fri, 27 Jan 2023 09:38:01 GMT
Content-Length: 507
Content-Type: text/html; charset=utf-8
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
<div>I asked: <i>"What's up?"</i></div>
<div><a href="/I%20asked:%20%3ci%3e%22What%27s%20up?%22%3c/i%3e">Path</a></div>
<div><a href="/?q=I%20asked%3a%20%3ci%3e%22What%27s%20up%3f%22%3c%2fi%3e">Query</a></div>
<div><a οnclick="f('I asked: \u003ci\u003e\u0022What\u0027s up?\u0022\u003c\/i\u003e')">Onclick</a></div>
</body>
</html>
持久性 XSS
漏洞(persistent XSS vulnerability),常见 XSS
攻击方式,由于服务器将攻击者存储的数据原原本本地显示给其它用户所致,比如攻击者数据含 <script>
标签,预防方法是对用户数据进行转义存储或显示。
例如按照 Go 模板语法定义创建一个 form.html
和 tmpl.html
模板文件,文件的具体内容如下:
<!-- form.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web</title>
</head>
<body>
<form action="/process" method="post">
Comment: <input name="comment" type="text" size="50">
<hr/>
<button id="submit">Submit</button>
</form>
</body>
</html>
<!-- tmpl.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web </title>
</head>
<body>
<div>{{ . }}</div>
</body>
</html>
编写一个 HTTP server 端程序解析模板文件进行渲染,该程序的具体内容如下:
package main
import (
"net/http"
"html/template"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, r.FormValue("comment"))
}
func form(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("form.html")
t.Execute(w, nil)
}
func main() {
http.HandleFunc("/process", process)
http.HandleFunc("/", form)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,在文本框输入
<script>alert('Pwnd!');<script>
,点击 Submint 按钮后浏览到页面内容如下:
<script>alert('Pwnd!');<script>
该网页的源码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Web </title>
</head>
<body>
<div><script>alert('Pwnd!');<script></div>
</body>
</html>
上下文感知的自动转义能让程序更加安全,比如防止 XSS
攻击(例如在表单中输入带有 <script>...</script>
的内容并提交,会使得用户提交的这部分 script
被执行),若不进行转义,可以使用下面的方式进行类型转换。
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, template.HTML(r.FormValue("comment")))
}
例如编写一个 HTTP server 端程序解析上面的 form.html
和 tmpl.html
模板文件进行渲染,该程序的具体内容如下:
package main
import (
"net/http"
"html/template"
"fmt"
)
func process(w http.ResponseWriter, r *http.Request) {
// 关闭浏览器内置的 XSS 防御功能
w.Header().Set("X-XSS-Protection", "0")
t, _ := template.ParseFiles("tmpl.html")
//不转换 HTML
t.Execute(w, template.HTML(r.FormValue("comment")))
}
func form(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("form.html")
t.Execute(w, nil)
}
func main() {
http.HandleFunc("/process", process)
http.HandleFunc("/", form)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Running HTTP server is failed: %v", err)
return
}
}
执行程序后访问指定网址 http://127.0.0.1:8080,在文本框输入
<script>alert('Pwnd!');<script>
,点击 Submint 按钮后跳转链接成功防止 XSS 攻击。
text/template 与 html/tempalte 的区别
在 Golang 的标准库中有两个和 template 有关的包, 一个是 html/template
,另外一个是 text/template
,这两个包的主要区别是 html/template
加入了很多对 js
字符串 和 html
标签的处理,针对的是需要返回 HTML 内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击。
例如编写一个程序来展示 text/template
的用法,该程序的具体内容如下:
package main
import (
"text/template"
"os"
)
func main() {
// 要注入的变量
type Inventory struct {
Material string
Count uint
}
sweaters := Inventory{"cqupthao", 1}
// 模板内容, {{.xxx}} 格式的都会被注入的变量替换
text := `{{.Count}} 位名叫 {{.Material}} 的同学!`
TestTemplate(text, sweaters)
}
func TestTemplate(text string, data interface{}) {
// 初始化,解析
tmpl, err := template.New("test").Parse(text)
if err != nil {
panic(err)
}
// 输出到 os.Stdout
err = tmpl.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
执行程序输出的结果如下:
1 位名叫 cqupthao 的同学!
-
参考链接 Go模板template用法详解
-
参考书籍:《Go Web 编程》 (谢孟军 著)
-
参考书籍:《Go Web 编程从入门到精通》 (廖显东 著)