golang中的标准库template
阅读原文时间:2022年04月06日阅读:1

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

模板

在基于MVC的Web架构中,我们通常需要在后端渲染一些数据到HTML文件中,从而实现动态的网页效果

模板示例

通过将模板应用于一个数据结构(即该数据结构作为模板的参数)来执行,来获得输出。模板中的注释引用数据接口的元素(一般如结构体的字段或者字典的键)来控制执行过程和获取需要呈现的值。模板执行时会遍历结构并将指针表示为’.‘(称之为”dot”)指向运行过程中数据结构的当前位置的值。

用作模板的输入文本必须是utf-8编码的文本。”Action”—数据运算和控制单位—由”“界定;在Action之外的所有文本都不做修改的拷贝到输出中。Action内部不能有换行,但注释可以有换行。

HTML文件代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <p>Hello {{.}}</p>
</body>
</html>

我们的HTTP server端代码如下:

func main() {
    http.HandleFunc("/", SayHello)
    err := http.ListenAndServe(":8000", nil)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
}
func SayHello(w http.ResponseWriter, r *http.Request) {
    // 解析指定文件,生成模板对象
    tmpl, err := template.ParseFiles("./templates/index.html")
    if err != nil {
        fmt.Println("create template failed, err: ", err)
        return
    }
    // 利用给定数据渲染模板,并将结果写入w
    tmpl.Execute(w, "sankuan")
}

模板语法

{{.}}

模板语法都包含在{{和}}中间,其中{{.}}中的点表示当前对象。

当我们传入一个结构体对象时,我们可以根据.来访问结构体的对应字段。例如:

func main() {
    http.HandleFunc("/", SayHello)
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println(err.Error())
        return
    }
}
func SayHello(w http.ResponseWriter, r *http.Request) {
    // 解析模板文件,生成模板对象
    tmpl, err := template.ParseFiles("./templates/index.html")
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    // 将数据渲染到模板,并写入到w
    user := User{"张三", 18, true}
    tmpl.Execute(w, &user)
}
type User struct {
    Name string
    Age uint8
    Gender bool
}

HTML文件代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <p>Name: {{.Name}}</p>
    <p>Age: {{.Age}}</p>
    <p>Gender: {{.Gender}}</p>
</body>
</html>

同理,当我们传入的变量是map时,也可以在模板文件中通过.根据key来取值。

注释

<p>Gender: {{/*.Gender*/}}</p>

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

pipeline

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

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

html中定义变量

Action里可以初始化一个变量来捕获管道的执行结果。初始化语法如下:

<body>
    <p>Name: {{.Name}}</p>
    <p>Age: {{.Age}}</p>
    <p>Gender: {{.Gender}}</p>  {{$score := 88}}
    <p>分数: {{$score}}</p>
</body>

if判断

{{if lt .Age 18}}未成年{{else}}已经成年{{end}}

golang的模板也支持if的条件判断,当前支持最简单的bool类型和字符串类型的判断

{{if .condition}} {{end}}

  当.condition为bool类型的时候,则为true表示执行,当.condition为string类型的时候,则非空表示执行。

当然也支持else , else if嵌套

{{if .condition1}} {{else if .contition2}} {{end}}

假设我们需要逻辑判断,比如与或、大小不等于等判断的时候,我们需要一些内置的模板函数来做这些工作,目前常用的一些内置模板函数有:

not 非

{{if not .condition}}

{{end}}

and 与

{{if and .condition1 .condition2}}

{{end}}

or 或

{{if or .condition1 .condition2}}

{{end}}

eq 等于

{{if eq .var1 .var2}}

{{end}}

ne 不等于

{{if ne .var1 .var2}}

{{end}}

lt 小于 (less than)

{{if lt .var1 .var2}}

{{end}}

le 小于等于

{{if le .var1 .var2}}

{{end}}

gt 大于

{{if gt .var1 .var2}}

{{end}}

ge 大于等于

{{if ge .var1 .var2}}

{{end}}

循环

main.go

func main() {
    http.HandleFunc("/", SayHello)
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println(err.Error())
        return
    }
}
func SayHello(w http.ResponseWriter, r *http.Request) {
    // 解析模板文件,生成模板对象
    tmpl, err := template.ParseFiles("./templates/index.html")
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    // 将数据渲染到模板,并写入到w
    user := User{"哈哈", 88, false, []int{11, 22, 33}, map[string]interface{}{
        "name": "定时发送放得开了",
        "age": 998,
    }}
    tmpl.Execute(w, &user)
}
type User struct {
    Name string
    Age uint8
    Gender bool
    Score []int
    Height map[string]interface{}
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
{{ .Name }}
{{ .Score }}

{{range .Score}}
{{.}}
{{end}}
{{range $i, $v := .Score}}
{{$i}}---{{$v}}
{{end}}

{{range .Height}}
{{.}}
{{end}}
{{range $k, $v := .Height}}
{{$k}}---{{$v}}
{{end}}
</body>
</html>

模板的嵌套

在编写模板的时候,我们常常将公用的模板进行整合,比如每一个页面都有导航栏和页脚,我们常常将其编写为一个单独的模块,让所有的页面进行导入,这样就不用重复的编写了。

任何网页都有一个主模板,然后我们可以在主模板内嵌入子模板来实现模块共享。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>

<!--定义子模板-->
{{define "navbar"}}
<h1>啊哈哈哈</h1>
<p>嘿嘿</p>
{{end}}

<!--使用子模板-->
{{template "navbar" .}}

</body>
</html>

模板中使用函数

golang的模板其实功能很有限,很多复杂的逻辑无法直接使用模板语法来表达,所以只能使用模板函数来绕过。

  首先,template包创建新的模板的时候,支持.Funcs方法来将自定义的函数集合导入到该模板中,后续通过该模板渲染的文件均支持直接调用这些函数。

该函数集合的定义为:

type FuncMap map[string]interface{}

key为方法的名字,value则为函数。这里函数的参数个数没有限制,但是对于返回值有所限制。有两种选择,一种是只有一个返回值,还有一种是有两个返回值,但是第二个返回值必须是error类型的。这两种函数的区别是第二个函数在模板中被调用的时候,假设模板函数的第二个参数的返回不为空,则该渲染步骤将会被打断并报错。

在模板文件内,调用方法也非常的简单:

{{funcname .arg1 .arg2}}

假设我们定义了一个函数

func add(left int, right int) int

则在模板文件内,通过调用

{{add 1 2}}

就可以获得取值: 3

这个结果,golang的预定义函数没有add,所以有点儿麻烦。

main.py

func main() {
    http.HandleFunc("/", SayHello)
    if err := http.ListenAndServe(":8000", nil); err != nil {
        fmt.Println(err.Error())
        return
    }
}
func SayHello(w http.ResponseWriter, r *http.Request) {
    // 解析模板文件,生成模板对象
    tmpl := template.New("./templates/index.html")
    tmpl.Funcs(template.FuncMap{
        "add": Add,
    })
    tmpl, err := tmpl.ParseFiles("./templates/index.html")
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    // 将数据渲染到模板,并写入到w
    user := User{"哈哈", 88, false, []int{11, 22, 33}, map[string]interface{}{
        "name": "定时发送放得开了",
        "age": 998,
    }}
    if err := tmpl.ExecuteTemplate(w, "index.html", user); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
    }

    //tmpl.Execute(w, &user)
}
type User struct {
    Name string
    Age uint8
    Gender bool
    Score []int
    Height map[string]interface{}
}
func Add(a, b int) int {
    return a + b
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>

哈哈哈哈

{{add 11 22}}

</body>
</html>

预定义函数

执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用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逸码等价表示。
urlquery
    返回其参数文本表示的可嵌入URL查询的逸码等价表示。
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”)。但是,整数和浮点数不能互相比较。