Go语言学习笔记【18】 Go语言常见库:html/template

本文介绍了Go语言的html/template库,包括模板引擎的基本概念、使用流程、基础语法、内置函数,以及模板嵌套和继承的详细操作。通过示例展示了如何定义、解析和渲染模板,以及如何处理模板中的条件判断、迭代、变量和注释。此外,还讨论了如何避免与前端框架的标识符冲突,以及模板继承的概念和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【声明】

非完全原创,部分内容来自于学习其他人的理论和B站视频。如果有侵权,请联系我,可以立即删除掉。

一、html/template

主要参考文档:
(1)Go语言标准库之http/template
(2)Go模板template用法详解

template包(html/template)实现了数据驱动的模板,用于生成可对抗代码注入的安全HTML输出。本包提供了和text/template包相同的接口,无论何时当输出是HTML的时候都应使用本包

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

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

1、模板引擎

1.1、简介

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

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

1.2、使用流程

分为三部分:定义模板文件、解析模板文件和模板渲染

1.2.1、定义模板文件

定义一个html文件即可,后缀名成可以设定为 tmpl 或者 tpl ,不过一般使用 tmpl 作为模板文件后缀

1.2.2、解析模板文件

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

func New(name string) *Template
//创建一个名为name的模板

func (t *Template) Funcs(funcMap FuncMap) *Template
//Funcs方法向模板t的函数字典里加入参数funcMap内的键值对。
//如果funcMap某个键值对的值不是函数类型或者返回值不符合要求会panic。
//但是,可以对t函数列表的成员进行重写。方法返回t以便进行链式调用

func (t *Template) Parse(src string) (*Template, error)
//Parse方法将字符串text解析为模板。嵌套定义的模板会关联到最顶层的t。
//Parse可以多次调用,但只有第一次调用可以包含空格、注释和模板定义之外的文本。
//如果后面的调用在解析后仍剩余文本会引发错误、返回nil且丢弃剩余文本;
//如果解析得到的模板已有相关联的同名模板,会覆盖掉原模板

func ParseFiles(filenames ...string) (*Template, error)
//ParseFiles函数创建一个模板并解析filenames指定的文件里的模板定义。
//返回的模板的名字是第一个文件的文件名(不含扩展名),内容为解析后的第一个文件的内容。
//至少要提供一个文件。如果发生错误,会停止解析并返回nil。

func ParseGlob(pattern string) (*Template, error)
//ParseGlob创建一个模板并解析匹配pattern的文件(参见glob规则)里的模板定义。
//返回的模板的名字是第一个匹配的文件的文件名(不含扩展名),内容为解析后的第一个文件的内容。至少要存在一个匹配的文件。
1.2.3、模板渲染

渲染模板简单来说就是使用数据去填充模板

func (t *Template) Execute(wr io.Writer, data interface{}) error
//Execute方法将解析好的模板应用到data上,并将输出写入wr。
//如果执行时出现错误,会停止执行,但有可能已经写入wr部分数据。模板可以安全的并发执行。

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
//ExecuteTemplate方法类似Execute,但是使用名为name的t关联的模板产生输出

1.3、使用示例

1.3.1、定义index.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>Person</title>
</head>
<body>
    <p> Id {{.p.Id}}</p>
    <p> Name {{.p.Name}}</p>
    <p> Age {{.p.Age}}</p>
    <p> Address {{.p.Addr}}</p>
</body>
</html>
1.3.2、代码解析
type Person struct {
	Id   int
	Name string
	Age  int
	Addr string
}

func main() {
	http.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
		// 根据模板文件生成模板对象
		tmpl, err := template.ParseFiles("index.tmpl")
		if err != nil {
			fmt.Println("parse template failed, err = ", err)
			return
		}
		// 构造数据渲染模板,将结果写入到w
		yufei := Person{
			Id:   20220821,
			Name: "YuFei",
			Age:  26,
			Addr: "WuHan",
		}
		//也可写为
		/*
			yufei = map[string]interface{}{
				"Id":   20220821,
				"Name": "YuFei",
				"Age":  26,
				"Addr": "WuHan",
			}
		*/
		err = tmpl.Execute(w, map[string]interface{}{
			"p": yufei,
		})
		if err != nil {
			fmt.Println("execute template failed, err = ", err)
			return
		}
	})
	if err := http.ListenAndServe(":8088", nil); err != nil {
		fmt.Println("http listen and accept failed, err = ", err)
		return
	}
}
1.3.3、运行结果

在这里插入图片描述

2、基础语法

2.1、{{.}}

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

<body>
    <p> Id {{.p.Id}}</p>
    <p> Name {{.p.Name}}</p>
    <p> Age {{.p.Age}}</p>
    <p> Address {{.p.Addr}}</p>
</body>

2.2、注释

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

2.3、pipeline

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

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

例如,下面的(len "output")pipeline,它整体先运行;{{.}}pipeline,它的结果将传递给printf,且传递的参数位置是"abcd"之后

{{println (len "output")}}
{{.}} | printf "%s\n" "abcd"

2.4、变量

可以在模板中声明变量,用来保存传入模板的数据或其他语句生成的结果

{{ $v := 66 }}
{{ $id := .p.Id}}

2.5、移除空格

template引擎在进行替换的时候,是完全按照文本格式进行替换的。除了需要评估和替换的地方,所有的行分隔符、空格等等空白都原样保留。使用{{-语法去除模板内容左侧的所有空白符号, 使用-}}去除模板内容右侧的所有空白符号。语法:{{- .Name -}} 。示例:
{{23 -}} < {{- 45}} => 23<45
注意-要紧挨{{}},同时与模板值之间需要使用空格分隔。

2.6、条件判断

有以下几种if条件判断语句,其中第三和第四是等价的。

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

2.7、range...end迭代

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

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

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

range的参数部分是pipeline,所以在迭代的过程中是可以进行赋值的。但有两种赋值情况:

{{range $value := .}}
{{range $key,$value := .}}

如果range中只赋值给一个变量,则这个变量是当前正在迭代元素的值
如果赋值给两个变量,则第一个变量是索引值(map/slice是数值,map是key),第二个变量是当前正在迭代元素的值。

2.8、with...end

with用来设置"."的值。两种格式

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

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

如:{{with "xx"}}{{println .}}{{end}}。代码将输出xx,因为"."已经设置为"xx"

2.9、内置函数

2.9.1、常见的内置函数
  • 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、printf、println
    分别等价于fmt包中的Sprint、Sprintf、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,模板执行会中断并返回给调用模板执行者该错误;
2.9.2、内置的比较函数
eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真

eq函数,支持多个参数:eq arg1 arg2 arg3 arg4...
它们都和第一个参数arg1进行比较。它等价于:arg1==arg2 || arg1==arg3 || arg1==arg4

2.10、修改默认的标识符

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

func (t *Template) Delims(left, right string) *Template
//Delims方法用于设置action的分界字符串,应用于之后的Parse、ParseFiles、ParseGlob方法。
//嵌套模板定义会继承这种分界符设置。空字符串分界符表示相应的默认分界符:{{或}}。返回值就是t,以便进行链式调用。

例如,将默认的标识符{{}}修改为{[]}
template.New("test").Delims("{[", "]}").ParseFiles("./t.tmpl")

2.11、使用示例

2.11.1、index.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>Person</title>
</head>
<body>
    {{/* 打印基本信息 */}}
    <p> Id: {{.Id}}</p>
    <p> Name: {{.Name}}</p>
    <p> Age: {{.Age}}</p>
    <p> Address: {{.Addr}}</p>
    {{/* .Name作为自定义函数fly的参数 */}}
    <p> Fly: {{fly .Name}}</p>

    <hr>
    {{ $age := .Age }}
    {{ if lt $age 20 }}
    好好学习, 天天向上
    {{ else if and (ge $age 20) (le $age 50) }}
    努力赚钱, 实现自由
    {{ else }}
    退休生活, 颐养天年
    {{ end }}

    <hr>
    {{ with .hobby}}
        {{/* with...end的范围内, '.'代表的就是'.hobby' */}}
        {{ range $i, $hobby := . }}
            <p>{{$i}} - {{$hobby}}</p>
        {{ else }}
            no hobby
        {{ end }}
    {{ end }}
</body>
</html>
2.11.2、加载模板的代码
import (
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
)

func main() {
	http.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
		//读取模板中的内容
		tmplbyte, err := ioutil.ReadFile("./index.tmpl")
		if err != nil {
			fmt.Println("ioutil.ReadFile index.tmpl failed, err = ", err)
			return
		}

		//自定义一个函数
		fly := func(name string) (string, error) {
			return name + " can fly", nil
		}
	
		//New创建模板对象,Funcs在这个对象里面进行注册函数,一定要在解析模板之前进行注册
		//采用链式操作在Parse之前调用Funcs添加自定义的fly函数
		tmpl, err := template.New("go").Funcs(template.FuncMap{"fly": fly}).Parse(string(tmplbyte))
		if err != nil {
			fmt.Println("create template failed, err = ", err)
			return
		}

		yufei := map[string]interface{}{
			"Id":    20220821,
			"Name":  "YuFei",
			"Age":   26,
			"Addr":  "WuHan",
			"hobby": []string{"乒乓球", "羽毛球", "排球"},
		}

		tmpl.Execute(w, yufei)
	})

	if err := http.ListenAndServe(":8088", nil); err != nil {
		fmt.Println("http listen and accept failed, err = ", err)
		return
	}
}
2.11.3、运行结果

浏览器的结果

3、模板嵌套

3.1、定义模板文件

3.1.1、``index.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<body>
    <h1>测试嵌套template语法</h1>
    <hr>
    {{template "ul.tmpl"}}
    <hr>
    {{template "ol.tmpl"}}
</body>
</html>

{{/* 通过define定义一个模板, 上面可直接调用 */}}
{{ define "ol.tmpl"}}
{{/* <ol> 标签与 <li> 标签一起使用, 创建有序列表 */}}
<ol>
    <li>王者</li>
    <li>吃鸡</li>
    <li>阴阳师</li>
</ol>
{{end}}
3.1.2、ul.tmpl
{{/* <ul> 标签与 <li> 标签一起使用,创建无序列表 */}}
<ul>
    <li>sleep</li>
    <li>eat</li>
    <li>drink</li>
</ul>

3.2、解析嵌套模板

注意:在解析模板时,被嵌套的模板一定要在后面解析

func main() {
	http.HandleFunc("/index", func(w http.ResponseWriter, r *http.Request) {
		//解析模板, 被嵌套的模板一定要在后面解析
		tmpl, err := template.ParseFiles("./index.tmpl", "./ul.tmpl")
		if err != nil {
			fmt.Println("create template failed, err = ", err)
			return
		}
		//模板渲染
		tmpl.Execute(w, nil)
	})
	if err := http.ListenAndServe(":8088", nil); err != nil {
		fmt.Println("http listen and accept failed, err = ", err)
		return
	}
}

3.3、运行结果

嵌套模板的渲染结果

4、block(模板继承)

根据官方文档的解释:block等价于define定义一个名为name的模板,并在"有需要"的地方执行这个模板,执行时将"."设置为pipeline的值。

但应该注意,block的第一个动作是执行名为name的模板,如果不存在,则在此处自动定义这个模板,并执行这个临时定义的模板。换句话说,block可以认为是设置一个默认模板,即父类模板

例如:{{block "T1" .}} one {{end}}。它先找到T1模板,如果T1存在,则执行找到的T1,如果没找到T1,则临时定义一个模板{{define "T1"}} one {{end}},并执行它。

在很多网页中,大体上的布局都差不多,因此可以将布局封装为一个默认模板,以供其他模板调用

4.1、示例格式

4.1.1、定义根模板

根模板templates/base.tmpl,其中的{{block "content" . }}{{end}}是子模板需要填充的内容

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Go Templates</title>
</head>
<body>
<div class="container-fluid">
    {{block "content" . }}{{end}}
</div>
</body>
</html>
4.1.2、定义子模板

定义一个templates/index.tmpl,”继承”base.tmpl

{{template "base.tmpl"}}

{{define "content"}}
    <div>Hello world!</div>
{{end}}
4.1.3、解析和渲染模板

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

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

4.2、网页布局模板的示例

4.2.1、定义根模板base.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>定义base模板</title>
    <style>
        * {
            margin: 0;
        }
        .nav {
            height: 50px;
            width: 100%;
            position: fixed;
            top: 0;
            background-color:deepskyblue;
        }
        .main {
            margin-top: 50px;
        }
        .menu {
            width: 20%;
            height: 100%;
            position: fixed;
            left: 0;
            background-color: lightgrey;
        }
        .center {
            text-align: center;
        }
    </style>
</head>
<body>
<div class = "nav"></div>
<div class="main">
    <div class = "menu"></div>
    <div class = "content center">
    	{{/* 此处为子模板预留位置进行填充 */}}
        {{block "content" .}}{{end}}
    </div>
</div>
</body>
</html>
4.2.2、定义子模板index.tmplhome.tmpl

index.tmpl

{{/* 继承根模板, 需要使用`.`将数据接收过来 */}}
{{template "base.tmpl" .}}

{{/* 重新定义块模板 */}}
{{define "content"}}
    <h1>这是index页面</h1>
    <p>{{.}}</p>
{{end}}

home.tmpl

{{/* 继承根模板, 需要使用`.`将数据接收过来 */}}
{{template "base.tmpl" .}}

{{/* 重新定义块模板 */}}
{{define "content"}}
    <h1>这是home页面</h1>
    <h3>{{.}}</h3>
{{end}}
4.2.3、解析模板
func indexFunc(w http.ResponseWriter, r *http.Request) {
	//定义模板文件
	//解析模板
	tmpl, err := template.ParseGlob("./*.tmpl")
	if err != nil {
		fmt.Println("parse template failed, err = ", err)
		return
	}
	//渲染模板
	tmpl.ExecuteTemplate(w, "index.tmpl", "this is a index page")
}

func homeFunc(w http.ResponseWriter, r *http.Request) {
	//定义模板文件
	//解析模板
	tmpl, err := template.ParseFiles("./base.tmpl", "./home.tmpl")
	if err != nil {
		fmt.Println("parse template failed, err = ", err)
		return
	}
	//渲染模板
	tmpl.ExecuteTemplate(w, "home.tmpl", "this is a home page")
}

func main() {
	http.HandleFunc("/index", indexFunc)
	http.HandleFunc("/home", homeFunc)
	if err := http.ListenAndServe(":8088", nil); err != nil {
		fmt.Println("listen failed, err = ", err)
		return
	}
}
4.2.4、运行结果

index页面
home页面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值