模板渲染是指根据特定的模板语法,将这些模板渲染成文本,通常用于生成 HTML、邮件、配置文件等。在Go语言中提供了两个标准库来渲染模板,它们都具有相同的模板语法,分别是
text/template
和html/template
,接下来将介绍这两个标准库如何使用。
快速使用
二者都是go语言的标准库,直接使用:
|
|
使用template库很简单,只需要创建Template
对象(模板不是文件需要指定模板的名称),调Parse
方法解析模板,最后调Execute
方法指定输出的地方和数据,就能渲染模板。
模板是一串字符串,由模板语法组成。模板语法都包含在{{
和}}
中间,其中{{.}}
中的点表示当前对象。输出的地方是一个io.Writer
。数据常用的是结构体对象或map类型。渲染模板是根据模板语法将数据绑定到模板中。
上面程序运行输出:
|
|
注释掉"text/template"
换成"html/template"
输出:
|
|
二者的主要区别就是html/template
会将渲染的HTML内容转义。意思就是让绑定的HTML数据变成普通文本,失去其原来的作用,能够防止跨站脚本攻击 (XSS)。因此当我们的模板是HTML时,推荐使用html/template
包渲染,并且该包也是专门为渲染HTML模板准备的,确保生成的 HTML 是安全的。
template介绍
在 Go 语言中,text/template
和 html/template
都是用于模板处理的包,它们在功能上有许多相似之处,但也有重要的区别,尤其是在处理 HTML 时。
作用:
text/template
:适用于生成纯文本内容,比如邮件、日志、配置文件等。这些内容不涉及 HTML 安全性问题。html/template
:专门用于生成 HTML 内容,提供了防止 XSS 攻击的保护机制。
共同点:
- 模板解析:两个包都使用类似的语法来解析模板,支持条件语句、循环、自定义函数等。
- 数据绑定:都可以将结构体、映射等数据绑定到模板中,从而生成动态内容。
- 模板执行:都使用
Execute
和ExecuteTemplate
方法来执行模板,将结果输出到io.Writer
。
不同点:
主要区别:
- 在模板中插入用户提供的数据时,
html/template
会自动对这些数据进行 HTML 转义,确保生成的 HTML 是安全的,能够防止跨站脚本攻击 (XSS)。而text/template
包不会。
- 在模板中插入用户提供的数据时,
内置的模板函数同名,但功能可能不一样。
跨站脚本攻击(XSS):大概意思是脚本不是自己网站提供的,是别人恶意放到你网站上的。
当用户访问你的网站时,恶意脚本就会运行。
恶意脚本可能做的事:
- 窃取 Cookie 和会话信息: 攻击者可以通过恶意脚本窃取用户的 Cookie 和会话信息,从而冒充用户进行操作。
- 劫持用户会话: 攻击者可以利用窃取的会话信息,劫持用户的会话,进行恶意操作。
- 伪造请求: 恶意脚本可以在用户不知情的情况下发起伪造请求,执行一些用户未授权的操作。
- 传播蠕虫: 恶意脚本可以通过 XSS 漏洞传播蠕虫,自动感染访问受害页面的用户。
- 虚假内容: 恶意脚本可以修改页面内容,显示虚假信息欺骗用户。
防御XSS攻击:
输出编码:
在将用户输入的数据输出到 HTML 内容时,进行 HTML 转义,防止恶意脚本注入。
html/template
包就会自动转义HTML内容。在属性中输出数据时,对数据进行属性转义。
输入验证:
- 对用户输入的数据进行严格验证和过滤,只允许符合预期格式的数据通过。
使用安全的 JavaScript:
- 避免直接使用
innerHTML
,改用textContent
或其他安全的 DOM 操作方法。内容安全策略 (Content Security Policy, CSP):
配置 CSP 头,通过白名单机制限制允许执行的脚本来源,减少 XSS 攻击的风险。
1
Content-Security-Policy: script-src 'self'
template使用
提前介绍模板语法中的定义模板:
模板就是一串字符串或一个文本文件。
当为一串字符串时,可能是主模板,也可能是子模板。
- 没有定义子模板时,是主模板。反之就是子模板。
当为一个文本文件时,相对文件,文件名就是主模板。
- 通常会将
目录/文件名
设为该文件的子模板,以表示主模板。防止不同目录出现同名文件。此时渲染整个文件,就需要指定子模板名。
- 通常会将
定义子模板:用define内置模板函数,后面更子模板名,是字符串类型,用双引号括起来。结束用end函数标识。中间就是子模板的内容。如:
templates/default/index.tmpl
:1 2 3
{{ define "default/index.tmpl" }} Hello, {{ .Name }}! Your message is: {{ .Message }} {{ end }}
此时渲染这个文件,就需要用指定模板名的方法。
更多内容参考。
渲染Template对象
渲染Template对象就是渲染模板,用到的方法是func (t *Template) Execute(wr io.Writer, data any) error
或func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error
。
二者的区别是:
Execute
方法用于渲染主模板。- 只有一个模板,这个模板就是主模板。
- 有多个模板时,定义或解析模板时最先创建的那个模板,或是执行时作为入口点的模板(没有指定模板名的都会先渲染),最先渲染的模板就是主模板。
ExecuteTemplate
方法用于渲染指定模板名的模板。
总结:
- 只有一个模板用
Execute
方法,也可以用ExecuteTemplate
方法,当需要指定模板名。 - 有多个模板用
ExecuteTemplate
方法,需要指定模板名。如果此时用Execute
方法将只渲染主模板。 - 用
func (t *Template) DefinedTemplates() string
方法可以查看定义的所以模板。返回的字符串格式为; defined templates are:...
- 只有一个模板用
创建Template对象
创建Template
对象的方法主要有以下几种:
解析并创建Template对象:
template.ParseFiles
: 从文件中解析模板并创建Template对象,可以一次解析一个或多个文件。1 2 3 4
t, err := template.ParseFiles("templates/file1.tmpl", "templates/file2.tmpl") if err != nil { // handle error }
template.ParseGlob
: 使用通配符模式解析一组模板文件并创建Template对象。(常用)所有模板都放在templates目录下(没有分类保存):
1 2 3 4
t, err := template.ParseGlob("templates/*.tmpl") if err != nil { // handle error }
所有模板都放在templates目录下并分类保存:
需要多匹配一级分类目录,目录用
/**/
表示该级目录(只能匹配一级)。两级用/**/**
,以此类推。如:1 2 3 4
t, err := template.ParseGlob("templates/**/*.tmpl") if err != nil { // handle error }
将匹配templates目录下的所有一级目录下的所有
*.tmpl
文件。分目录保存,为防止不同目录出现同文件名,造成模板丢失,通常会将
目录/文件名
设为该文件的子模板,以表示主模板。防止不同目录出现同名文件。此时渲染整个文件,就需要指定子模板名。
创建空的命名模板对象:
template.New
: 创建一个命名模板的基础模板对象,不会解析任何模板内容。- 创建命名模板对象之后,还需要解析模板才能使用。
- 通常与
Parse
(常用)、ParseFiles
、ParseGlob
等方法一起使用。使用方法与同名函数一直。只要是Template对象都能使用这几个方法。 - 区别是:
- 用
New
方法创建空的命名模板后(这模板就是主模板),在用Parse
等方法解析模板:- 如果解析的模板没有命名且非空,则会覆盖主模板的内容。
- 如果解析的模板有命名,则会为主模板添加子模板。
- 以上情况为字符串模板。
- 解析文件都是为主模板添加子模板。因为文件名默认为一个模板名。
- 用
template.Must
: 是一个帮助函数,用于包装解析函数(如Parse
、ParseFiles
、ParseGlob
等)的返回值。如果解析过程中发生错误,它会导致程序在运行时崩溃。常用于简化创建模板对象代码。
示例:
|
|
模板语法介绍
基础语法
模板语法都包含在
{{
和}}
中间。{{.}}
:点表示当前对象。数据是结构体对象时,直接用
.属性名
访问属性。数据是映射时,直接用
.键名
访问值。键名或属性名要一一对应。
以上两个可以互相嵌套访问。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
package main import ( "log" "os" "text/template" //"html/template" ) type User struct { Name string Age int Other map[string]any } func main() { t := template.Must(template.New("example").Parse(`{{ .title }} 姓名:{{ .user.Name }} 年龄:{{ .user.Age }} QQ: {{ .user.Other.QQ }} 学校:{{ .user.Other.school }}`)) err := t.Execute(os.Stdout, map[string]any{ "title": "用户信息", "user": User{ Name: "Jack", Age: 18, Other: map[string]any{ "QQ": 9527, "school": "野鸡大学", }, }, }) if err != nil { log.Fatalf("Excuting templete: %s", err) } }
运行输出:
1 2 3 4 5 6
$ go run main.go 用户信息 姓名:Jack 年龄:18 QQ: 9527 学校:野鸡大学
数据是基本数据类型时,如int、bool、string。直接用
.
表示值。数据是切片、数组、映射等集合类型时,需要遍历对象。(稍后介绍)
- 注意映射有两种方法访问值:
.键名
或者循环遍历
。
- 注意映射有两种方法访问值:
格式:
{{ 模板表达式 }}
。模板表达式与括号之间建议用空格隔开。如果有空格渲染时会自动移除。模板表达式可以是
.
、函数、变量等组成。表示式如果没有值,渲染后会用
<no value>
表示。在两个括号内添加
-
(与括号之间不能有空格,与表达式之间必须有空格)表示删除空白。- 删除的是渲染结果与周围字符之间的空白。
-
在那边就表示删除那边与字符之间的空白。都有表示左右两边字符都删除。- 空白指空格、换行。
1 2
"{{ 23 -}} < {{ 45 }}" // 输出23< 45 "{{ 23 -}} < {{- 45 }}" // 输出23<45
注释格式:
{{/* 注释内容 */}}
。注意/
与括号之间不能有空格。执行时会忽略。可以多行。注释不能嵌套。*与注释内容可以没有空格。定义模板前面已经介绍了。
- 补充:定义子模板语句如果独占一行,虽然渲染后为空,但是会独占一空行,注释也会,后面的定义变量也是。
- 为不影响渲染之后的结构,可以将他们与内容写在一行。这样即使注释换行也不会占一空行。模板语法与其他内容之间也不要有空格,否则渲染之后会保留空格。
- 后来才知道删除空格的作用。最优解决方案:使用用
-
可以删除空格、换行。能达到美观而不影响渲染结果。
模板变量
在模版中可以自定义变量, 类似golang使用:=符号定义变量,用来保存传入模板的数据或其他语句生成的结果。语法为:
{{ $变量名 := 数据 }}
为存在的变量赋值
{{ $变量名 = 数据 }}
引用变量
{{ $变量名 }}
数据可以是字符串(用双引号括起来)、整型、表达式的值。
引用变量可以与表达式组合使用。
上面示例可修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
package main import ( "log" "os" //"text/template" "html/template" ) type User struct { Name string Age int Other map[string]any } func main() { t := template.Must(template.New("example"). Parse(`{{ define "aaa" }} {{ .title }} {{ $other := .user.Other }} 姓名:{{ .user.Name }}{{/* 这是注释 换行了 */}} 年龄:{{ .user.Age }} QQ: {{ $other.QQ }} 学校:{{ $other.school }}{{ end }}`)) err := t.ExecuteTemplate(os.Stdout, "aaa", map[string]any{ "title": "用户信息", "user": User{ Name: "Jack", Age: 18, Other: map[string]any{ "QQ": 9527, "school": "野鸡大学", }, }, }) if err != nil { log.Fatalf("Excuting templete: %s", err) } }
运行输出:
1 2 3 4 5 6 7
$ go run main.go 用户信息 姓名:Jack 年龄:18 QQ: 9527 学校:野鸡大学
可以对比一下有什么区别。(定义子模块的头部独占一空行,解决办法:与title同行且在前面)
可以不用修改结构,使用
-
删除空白。推荐使用
流程控制语句
介绍流程控制语句前,先介绍比较函数(也叫逻辑运算函数)。将逻辑运算封装成了函数形式。
常用的比较函数如下:
- eq:等于
- ne:不等于
- lt:小于
- le:小于等于
- gt:大于
- ge:大于调用
比较函数后面跟两个可比较的参数,用空格分隔开。返回true或者false,对应类型的零值为false,其余为true。如:
|
|
比较函数可以与条件判断(if语句)组合使用。
模版语法的流程控制语句主要指if/range/with三种语句。
条件判断
if
( else
, else if
)语句用于根据条件来控制模板的输出。可以使用else
和else if
来处理其他情况。
格式:
|
|
if
语句后面跟end
表示结束。
示例:
|
|
运行输出:
|
|
循环
循环range
语句用于迭代数组、切片、映射等集合类型的数据。跟go语言的for-range
差不多,甚至比go语言更简洁。
格式:
|
|
遍历集合类型数据之后,可以用.
访问每一个集合元素的值。
注意:range
范围内,.
的作用域仅在range
范围内,无法访问外部对象。
如果需要访问索引,可以使用两个变量接收:
|
|
range
语句后面跟end
表示结束。它们之间还可以嵌入else
语句,当集合类型数据长度为0时将执行else
语句。
|
|
与if
语句组合使用:
|
|
示例1:
|
|
示例2:
|
|
运行输出:
|
|
变量声明(with
, define
, block
)
with
with
语句用于重定义模板的作用域。常用于缩短长的字段访问路径。就是可以缩短结构体对象属性的访问路径。简单理解:重定义.
的作用域(var . := .User
):将当前对象的User
属性复制给.
。如:
|
|
注意:range和with语句都改变了点(.)引用的数据,那么如果想要在range和with语句中引用模版参数,请先将(点(.)赋值给一个自定义变量, 然后在range和with中通过自定义变量,引用模版参数。
示例:
|
|
运行输出:
|
|
define
define
语句用于定义一个模板,通常用于复用模板片段,实现模板嵌套。
|
|
可以在其他地方使用template
来引用:
|
|
template函数的第一个参数是模板名字,第二个参数是当前模板参数, 在子模板内部也是通过点( . ),引用模板参数。
当子模板没有参数时,.
是可选的。
注意:
- 引入子模板带参数的时候别忘记最后的
.
。用于传递模板参数。 - 定义子模版不能嵌套。
示例:
|
|
运行输出:
|
|
模板管理
上面的例子,我们将模板代码定义在一个变量或者常量中,这个只是用于演示,实际项目中模板代码通常非常多,建议大家按如下方式组织模板代码:
- 一个模板的模板代码,保存在一个模板文件中,模板文件名后缀为tpl或tmpl或者其他,如html。编码方式是utf-8。
- 所有的模板代码都定义在子模板中,方便根据模板名字进行渲染。
- 所以模板都建议放在templates目录下。使用
ParseGlob
匹配模式批量解析模板并创建模板对象。 - 可以将公共的子模板定义在一个文件中
common.tpl
。
示例:
模板目录templates, 下面分别按功能模块创建不同的模板文件。
创建公共模板文件: templates/common.tpl。 主要用于保存一些公共的模板定义:
|
|
创建mod1模块的模板文件: templates/mod1.tpl:
|
|
创建mod2模块的模板文件: templates/mod2.tpl:
|
|
渲染模板代码:
|
|
运行输出:
|
|
block
block
语句用于定义一个可重写的模板块。通常用于嵌套模板或布局模板。意思就是:没有重写的模板将使用默认模板渲染。
格式:
|
|
子模板可以重写block
:
|
|
注意事项:
- 定义可重写的模板块,用
block
语句,后面跟两个参数:重写的模块名和传递的模板参数(.
)。二中缺一不可。block
语句通常与在define
语句组合使用,用于在定义模板中定义可重写的模板。
- 重写模板用
define
语句。语法格式与定义模板一样。- 需要注意的是不能嵌套
define
语句。
- 需要注意的是不能嵌套
- 重写模板与定义重写模板不能在同一个模板文件中。
- 确保
define
语句在模板文件的顶层定义。 block
语句仅在go版本1.19或更高版本以上支持text/template
和html/template
,以外版本仅支持html/template
包或都不支持。
示例:
templates/default/base.tmpl:
|
|
templates/default/index.tmpl:
|
|
main.go:
|
|
运行输出:
|
|
实现重写模板。
模板函数介绍
go的模板引擎为我们提供了函数机制,方面我们在处理模板时执行一些特定的功能,例如格式化输出内容、字母大小写转换等等。
模板函数调用语法
语法格式:
|
|
Argument参数是可选的,如果有多个参数,参数直接用空格分隔。
注意:模板语法都是在{{}}
中的,函数调用也是。
示例:
|
|
html预定义函数,将html内容进行转义,防止XSS攻击。
渲染将输出:
|
|
多个函数参数的示例:
|
|
printf函数主要用于格式化输出字符串,是fmt.Sprintf函数的别名,用法跟fmt.Sprintf函数一样,区别就是模板函数的参数用空格隔开。
这里为printf函数传递了3个参数。
渲染将输出:
|
|
预定义模板函数
预定义模板函数也可以叫内置模板函数,是模板引擎预定义好了的,可以直接在模板中拿来使用。下面介绍常用的内置函数:
前面介绍的比较函数(关系运算函数)也是属于预定义函数。
也将逻辑运算封装成了函数形式:
1 2 3 4 5 6
and 表达式1 表达式2 表达式1和表达式2都为真的时候返回true or 表达式1 表达式2 表达式1和表达式2其中一个为真的时候返回true not 表达式 表达式为false则返回true, 反之返回false
官网解释看不明白,差不多就是上面的意思。
提示: 关系运算和逻辑运算函数通常跟
if
语句一起使用。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
{{$x := 100}} //等价于$x == 100 {{if eq $x 100}} ...代码... {{end}} //等价于$x < 100 {{if lt $x 500}} ...代码... {{end}} //等价于$x >= 100 {{if ge $x 500}} ...代码... {{end}} //等价于$x > 50 && $x < 200 //这里调用了and函数和gt、lt三个函数, gt和lt函数的结果作为and的参数,gt和lt函数调用分别用括号包括起来 {{if and (gt $x 50) (lt $x 200)}} ...代码... {{end}} {{$y := 200}} //等价于$x > 100 || $y > 100 {{if or (gt $x 100) (gt $y 100)}} ...代码... {{end}}
更多内置函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
len 返回它的参数的整数类型长度,可以计算数组长度。 数组大小: {{len .}} //模板参数定义如下: a := []int{1,2,3,4} 渲染输出: 数组大小: 4 index 返回其第一个参数(通常为数组、切片或映射)的第 N 个元素,N 由后续参数指定。 如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。 data := []string{"first", "second", "third"} tmpl := `Second element: {{ index . 1 }}` 渲染输出:Second element: second print 即fmt.Sprint printf 即fmt.Sprintf println 即fmt.Sprintln 主要用于格式化字符串,是go fmt.Sprintf函数的别名,前面的例子已经介绍。 html 返回与其参数的文本表示形式等效的转义HTML。 将其参数作为安全的 HTML 输出。 这个函数在html/template中不可用。 urlquery 以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。 将其参数编码为 URL 查询参数。 这个函数在html/template中不可用。 主要用于url编码。 /search?keyword={{urlquery "搜索关键词"}} /search?keyword=%E6%90%9C%E7%B4%A2%E5%85%B3%E9%94%AE%E8%AF%8D js 返回与其参数的文本表示形式等效的转义JavaScript。 将其参数作为安全的 JavaScript 输出。 {{ js "<script>alert('Hello, World!');</script>" }} 渲染输出: \u003Cscript\u003Ealert(\'Hello, World!\');\u003C/script\u003E call 调用其第一个参数指定的函数,其余参数作为函数参数传递。 执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2); 其中Y是函数类型的字段或者字典的值,或者其他类似情况; call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同); 该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型; 如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;
call示例(可以先看自定义模板函数部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
package main import ( "fmt" "os" "text/template" ) func sayHello(name string) string { return fmt.Sprintf("Hello, %s!", name) } func main() { tmpl := `{{ call . "Gopher" }}` t := template.Must(template.New("example").Funcs(template.FuncMap{ "sayHello": sayHello, }).Parse(tmpl)) t.Execute(os.Stdout, sayHello) }
运行输出:
1
Hello, Gopher!
管道(pipeline)
pipeline 翻译过来可以称为管道或者流水线, pipeline运算的作用是将多个函数调用或者值串起来,从左往右执行,左边执行的结果会传递给右边,形成一个任务流水。
pipeline运算符:| (竖线)
语法格式:
|
|
command可以是一个值,也可以是一个函数。
示例1:
|
|
这里意思就是将第一个字符串值传递给html函数。
渲染将输出:
|
|
示例2:
|
|
这个例子就是先将 “关键词” 传递给html函数转义下html标签,然后在将html执行结果传递给urlquery函数进行url编码。
渲染将输出:
|
|
注意:如果函数有多个参数,pipeline运算会将值传递给函数的最后一个参数, 例如: {{ 100 | printf "value=%d" }}
, 这里将100传递给printf函数的最后一个参数。
自定义模板函数
内置的模板函数使用有限,我们可以自己定义模板函数。
步骤:
创建自定义函数。
将自定义函数映射到模板引擎中。用
FuncMap
函数映射。本质是map类型。可以映射多个。- 键值都是自定义的函数名。
最后调用
Funcs
方法,将映射添加到模板中。注意请在解析前完成以上操作。
1 2 3 4 5 6 7 8 9
// 创建模板并添加自定义函数 t := template.New("example").Funcs(funcMap) // 解析匹配指定模式的模板文件 t = template.Must(t.ParseGlob("templates/*.tmpl")) or // 创建模板并解析 t := template.Must(template.New("example").Funcs(funcMap).Parse(tmpl))
示例:
|
|
运行输出:
|
|
修改默认的标识符
Go标准库的模板引擎使用的花括号{{
和}}
作为标识,而许多前端框架(如Vue
和 AngularJS
)也使用{{
和}}
作为标识符,所以当我们同时使用Go语言模板引擎和以上前端框架时就会出现冲突,这个时候我们需要修改标识符,修改前端的或者修改Go语言的。这里演示如何修改Go语言模板引擎默认的标识符:
|
|
用到的方法是Delims
,分别接收两端的分隔符为参数。
应用
在Go语言中,text/template
和html/template
包用于生成文本和HTML输出,常见的应用场景如下:
text/template
包:
生成配置文件:
通过模板生成动态配置文件,如YAML、JSON等格式,用于不同环境的配置管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
package main import ( "os" "text/template" ) func main() { tmpl, err := template.New("config").Parse(` apiVersion: v1 kind: ConfigMap metadata: name: {{.Name}} data: key: {{.Value}} `) if err != nil { panic(err) } data := struct { Name string Value string }{ Name: "example-config", Value: "example-value", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
生成代码:
自动生成代码文件,如生成CRUD代码、接口实现代码等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
package main import ( "os" "text/template" ) func main() { tmpl, err := template.New("crud").Parse(` package main type {{.Name}} struct { ID int Name string } func ({{.Receiver}} *{{.Name}}) Create() { // Create logic } func ({{.Receiver}} *{{.Name}}) Read() { // Read logic } func ({{.Receiver}} *{{.Name}}) Update() { // Update logic } func ({{.Receiver}} *{{.Name}}) Delete() { // Delete logic } `) if err != nil { panic(err) } data := struct { Name string Receiver string }{ Name: "User", Receiver: "u", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
生成文档:
生成报告、邮件、日志等文本文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
package main import ( "os" "text/template" ) func main() { tmpl, err := template.New("report").Parse(` Report: Name: {{.Name}} Date: {{.Date}} Summary: {{.Summary}} `) if err != nil { panic(err) } data := struct { Name string Date string Summary string }{ Name: "John Doe", Date: "2024-06-10", Summary: "This is a summary of the report.", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
html/template
包:
html/template
包专门用于生成安全的HTML内容,防止XSS(跨站脚本攻击)。以下是一些常见应用:
生成动态网页:
根据用户输入或数据库内容动态生成HTML页面,适用于Web应用的前端展示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
package main import ( "html/template" "net/http" ) type User struct { Name string Email string } func handler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.New("user").Parse(` <!DOCTYPE html> <html> <head> <title>User Page</title> </head> <body> <h1>Hello, {{.Name}}</h1> <p>Email: {{.Email}}</p> </body> </html> `) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } user := User{Name: "Alice", Email: "alice@example.com"} err = tmpl.Execute(w, user) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":8080", nil) }
生成邮件内容:
生成包含HTML格式的邮件内容,用于发送动态邮件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
package main import ( "bytes" "fmt" "html/template" "net/smtp" ) func main() { tmpl, err := template.New("email").Parse(` <html> <body> <h1>Hello, {{.Name}}</h1> <p>Thank you for joining our service.</p> </body> </html> `) if err != nil { panic(err) } var body bytes.Buffer data := struct { Name string }{Name: "Alice"} err = tmpl.Execute(&body, data) if err != nil { panic(err) } auth := smtp.PlainAuth("", "your-email@example.com", "your-email-password", "smtp.example.com") err = smtp.SendMail("smtp.example.com:587", auth, "your-email@example.com", []string{"recipient@example.com"}, body.Bytes()) if err != nil { panic(err) } fmt.Println("Email sent successfully!") }
生成HTML报告:
根据模板生成包含动态内容的HTML报告,适用于生成分析结果、统计数据展示等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
package main import ( "html/template" "os" ) func main() { tmpl, err := template.New("report").Parse(` <!DOCTYPE html> <html> <head> <title>Report</title> </head> <body> <h1>Report Summary</h1> <p>Name: {{.Name}}</p> <p>Date: {{.Date}}</p> <p>Summary: {{.Summary}}</p> </body> </html> `) if err != nil { panic(err) } data := struct { Name string Date string Summary string }{ Name: "John Doe", Date: "2024-06-10", Summary: "This is a summary of the report.", } err = tmpl.Execute(os.Stdout, data) if err != nil { panic(err) } }
总结
text/template
主要用于生成纯文本内容,如配置文件、代码、文档等。html/template
主要用于生成安全的HTML内容,如动态网页、邮件内容、HTML报告等。
这两个包都通过模板提供了强大的文本处理功能,可以根据需要选择适用的包来生成所需的输出。
补充
不转义HTML内容
如果你需要安全地显示用户输入的 HTML 内容,可以使用以下几种方法:
明确信任的内容:
当我们使用
html/template
包渲染时,会自动转义HTML内容,如果不希望转义HTML内容,可以将要渲染的HTML内容定义为template.HTML
类型(本质就是字符串),这样就不会自动转义HTML内容。需要注意的是:对于完全可信的内容,才能使用
template.HTML
,以防止XSS攻击。对应的信任JavaScript类型
template.JS
:不会自动转义为安全的js代码。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
package main import ( "log" "os" //"text/template" "html/template" ) func main() { const tmpl = "Hello, {{.Name}}! Your message is: {{.Message}}" data := struct { Name string Message template.HTML }{ Name: "World", Message: "<script>alert('XSS');</script>", } t := template.New("example") t, err := t.Parse(tmpl) if err != nil { log.Fatalf("Parsing template: %s", err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf("Excuting templete: %s", err) } }
运行输出:
1 2
$ go run main.go Hello, World! Your message is: <script>alert('XSS');</script>
使用 HTML 白名单:
如果用户提供的内容需要部分 HTML 标签,可以使用 HTML 解析库将用户输入的内容过滤,只允许特定的标签和属性通过。这可以使用第三方库如 bluemonday 来实现。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
package main import ( "html/template" "log" "os" "github.com/microcosm-cc/bluemonday" ) func sanitizeHTML(input string) template.HTML { p := bluemonday.UGCPolicy() // 使用默认的用户生成内容策略 sanitized := p.Sanitize(input) return template.HTML(sanitized) } func main() { const tmpl = `Hello, {{.Name}}! Your message is: {{.Message}}` data := struct { Name string Message template.HTML }{ Name: "World", Message: sanitizeHTML("<strong>Bold Text</strong>" + "<script>alert('XSS');</script>"), } t, err := template.New("example").Parse(tmpl) if err != nil { log.Fatalf("parsing template: %s", err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf("executing template: %s", err) } }
运行输出:
1 2
$ go run main.go Hello, World! Your message is: <strong>Bold Text</strong>
使用 Markdown:
另一种方式是使用 Markdown,将用户输入的 Markdown 转换为安全的 HTML。这种方式适用于允许用户使用简单标记语言格式化内容的场景。用到的第三方库是blackfriday。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
package main import ( "html/template" "log" "os" "github.com/russross/blackfriday/v2" ) func markdownToHTML(input string) template.HTML { output := blackfriday.Run([]byte(input)) return template.HTML(output) } func main() { const tmpl = `Hello, {{.Name}}! Your message is: {{.Message}}` data := struct { Name string Message template.HTML }{ Name: "World", Message: markdownToHTML("**Bold Text**\n\n" + "<script>alert('XSS');</script>"), } t, err := template.New("example").Parse(tmpl) if err != nil { log.Fatalf("parsing template: %s", err) } err = t.Execute(os.Stdout, data) if err != nil { log.Fatalf("executing template: %s", err) } }
运行输出:
1 2 3 4
$ go run main.go Hello, World! Your message is: <p><strong>Bold Text</strong></p> <p><script>alert(‘XSS’);</script></p>
在这个例子中,任何脚本标签或其他潜在的 XSS 攻击内容都会被转义,确保生成的 HTML 是安全的。
总结
- 明确信任的内容:对于完全可信的内容,使用
template.HTML
。 - 使用 HTML 白名单:使用 HTML 解析库,如
bluemonday
,过滤用户输入,只允许特定的标签和属性。 - 使用 Markdown:将用户输入的 Markdown 转换为安全的 HTML,避免直接嵌入用户提供的 HTML。
这几种方法可以在保证安全性的前提下,允许一定程度的 HTML 内容显示。选择具体方法时,应根据应用场景和安全需求做出适当选择。