Go Template 使用


模板引擎的使用


在 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 语言的模板语法中可以使用 rangewith 关键字进行遍历,其中 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 标准库的模板引擎使用的花括号 {{}} 作为标识,许多前端框架(如 VueAngularJS )也使用 {{}} 作为标识符,当同时使用 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 明显不同);
    该函数类型值必须有 12 个返回值,如果有 2 个则后一个必须是 error 接口类型;
    如果有 2 个返回值的方法返回的 errornil ,模板执行会中断并返回给调用模板执行者该错误;

比较函数

布尔函数会将任何类型的零值视为假,其余视为真,下面是定义为函数的二元比较运算的集合:

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.htmlblue.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: &lt;i&gt;&#34;What&#39;s up?&#34;&lt;/i&gt;</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.htmltmpl.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>&lt;script&gt;alert(&#39;Pwnd!&#39;);&lt;script&gt;</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.htmltmpl.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 的同学!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Go Template是Go语言提供的一种轻量级的模板语言,它支持将数据和模板分离,使得程序的可维护性和可扩展性得到很大的提升,同时也避免了代码中出现大量的格式化字符串和HTML代码等。 Go Template的语法简洁、灵活,可以很好地适应不同的需求,支持条件判断、循环、变量定义、函数调用等常见的模板语法,同时还支持自定义函数和管道操作,可以方便地扩展模板的功能。 使用Go Template可以快速地生成各种格式的输出,例如HTML页面、XML文档、JSON数据等,它还可以与其他Go语言的库和框架无缝集成,如Web框架、数据库操作库等。 下面是一个简单的Go Template示例: ``` {{define "header"}} <html> <head> <title>{{.Title}}</title> </head> <body> {{end}} {{define "footer"}} </body> </html> {{end}} {{template "header" .}} <h1>Hello, {{.Name}}!</h1> {{template "footer" .}} ``` 这个模板定义了两个模板块,分别是“header”和“footer”,通过{{define}}关键字定义,可以在模板中被引用。在模板中使用{{.}}表示当前数据上下文,可以通过点操作符来访问数据结构中的字段。 使用{{template}}关键字可以在模板中引用其他模板块,并将当前上下文传递给被引用的模板。在上面的示例中,我们先引用了“header”模板块,然后输出了一个hello消息,最后引用了“footer”模板块。 要使用Go Template,只需要将模板字符串传递给template.Parse()函数,然后使用Execute()函数将数据结构传递给模板即可。例如: ``` t, err := template.New("").Parse(templateString) if err != nil { // 处理错误 } data := struct { Title string Name string }{ "Welcome", "John", } err = t.Execute(os.Stdout, data) if err != nil { // 处理错误 } ``` 这个示例演示了如何将模板字符串解析为模板对象,然后用一个数据结构填充模板并输出到标准输出。实际应用中,可以将模板输出到文件、网络连接或者HTTP响应中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

물の韜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值