在上一篇文章中,咱们介绍了 Go 模板库text/template
。text/template
库用于生成文本输出。在 Web 开发中,涉及到不少安全方面的问题。有些数据是用户输入的,不能直接替换到模板中,不然可能致使注入攻击。Go 提供了html/template
库处理这些问题。html/template
提供了与text/template
同样的接口。咱们一般使用html/template
生成 HTML 输出。html
因为上一篇文章已经详细介绍了 Go 模板的基本概念,本文主要从使用的层面来介绍html/template
库。中间会有安全方面的内容。那就开始吧!git
html/template
库的使用与text/template
基本同样:github
package main
import (
"fmt"
"html/template"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("hello.html")
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
t.Execute(w, "Hello World")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}复制代码
模板文件hello.html
:golang
<!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>
{{ . }}
</body>
</html>复制代码
模板中的{{ . }}
会被替换为传入的数据"Hello World",程序将模板执行后生成的文本经过ResponseWriter
传回客户端。web
编译,运行程序(个人环境 Win10 + Git Bash):编程
$ go build -o main.exe main.go
$ ./main.exe复制代码
打开浏览器,输入localhost:8080
,便可看到"Hello World"页面。浏览器
咱们在介绍text/template
库的时候,提到了空白问题。在生成 HTML 时通常不须要考虑这个问题,由于浏览器渲染的时候会自动去掉多余的空白。安全
为了编写示例代码的便利,在解析时不进行错误处理,html/template
库提供了Must
方法。它接受两个参数,一个模板对象指针,一个错误。若是错误参数不为nil
,直接 panic,不然返回模板对象指针。使用Must
方法简化上面的处理器:微信
func indexHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("hello.html"))
t.Execute(w, "Hello World")
}复制代码
接下来,咱们经过示例再过一遍几种动做。curl
func conditionHandler(w http.ResponseWriter, r *http.Request) {
age, err := strconv.ParseInt(r.URL.Query().Get("age"), 10, 64)
if err != nil {
fmt.Fprint(w, err)
return
}
t := template.Must(template.ParseFiles("condition.html"))
t.Execute(w, age)
}
mux.HandleFunc("/condition", conditionHandler)复制代码
模板文件 condition.html 只有 body 部分不一样:
<p>Your age is: {{ . }}</p>
{{ if gt . 60 }}
<p>Old People!</p>
{{ else if gt . 40 }}
<p>Middle Aged!</p>
{{ else }}
<p>Young!</p>
{{ end }}复制代码
模板逻辑很简单,使用内置函数gt
判断传入的年龄处于哪一个区间,显示对应的文本。
编译、运行程序,打开浏览器,输入localhost:8080/condition?age=10
。
迭代动做通常用于生成一个列表。
type Item struct {
Name string
Price int
}
func iterateHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("iterate.html"))
items := []Item {
{ "iPhone", 5499 },
{ "iPad", 6331 },
{ "iWatch", 1499 },
{ "MacBook", 8250 },
}
t.Execute(w, items)
}
mux.HandleFunc("/iterate", iterateHandler)复制代码
模板文件iterate.html
:
<h1>Apple Products</h1>
<ul>
{{ range . }}
<li>{{ .Name }}: ¥{{ .Price }}</li>
{{ end }}
</ul>复制代码
再次提醒,在{{ range }}
中,.
会被替换为当前遍历的元素值。
设置动做容许用户在指定范围内为.
设置值。
type User struct {
Name string
Age int
}
type Pet struct {
Name string
Age int
Owner User
}
func setHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("set.html"))
pet := Pet {
Name: "Orange",
Age: 2,
Owner: User {
Name: "dj",
Age: 28,
},
}
t.Execute(w, pet)
}
mux.HandleFunc("/set", setHandler)复制代码
模板文件set.html
:
<h1>Pet Info</h1>
<p>Name: {{ .Name }}</p>
<p>Age: {{ .Age }}</p>
<p>Owner:</p>
{{ with .Owner }}
<p>Name: {{ .Name }}</p>
<p>Age: {{ .Age }}</p>
{{ end }}复制代码
在{{ with .Owner }}
和{{ end }}
之间,能够直接经过{{ .Name }}
和{{ .Age }}
访问宠物主人的信息。
包含动做容许用户在一个模板里面包含另外一个模板,从而构造出嵌套的模板。
func includeHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("include1.html", "include2.html"))
t.Execute(w, "Hello World!")
}复制代码
模板include1.html
:
<!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>
<div>This is in template include1.html</div>
<p>The value of dot is {{ . }}</p>
<hr/>
<p>Don't pass argument to include2.html:</p>
{{ template "include2.html" }}
<hr/>
<p>Pass dot to include2.html</p>
{{ template "include2.html" . }}
<hr/>
</body>
</html>复制代码
模板include2.html
:
<p>Get dot of value [{{ . }}]</p>复制代码
建议本身动手运行一下程序,观察输出。
{{ template "include2.html" }}
未传入参数给模板include2.html
,{{ template "include2.html" . }}
将模板include1.html
的参数传给了include2.html
。
管道咱们能够理解为数据的流向,在数据流向输出的每一个阶段进行特定的处理。
func pipelineHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("pipeline.html"))
t.Execute(w, rand.Float64())
}
mux.HandleFunc("/pipeline", pipelineHandler)复制代码
模板文件pipeline.html
:
<p>{{ . | printf "%.2f" }}</p>复制代码
该程序实现的功能很是简单,将传入的浮点数格式化为只保留小数点后两位。|
是管道符号,前面的输出将做为后面的输入(若是是函数或方法调用,前面的输出将做为最后一个参数)。实际上,{{ . | printf "%.2f" }}
的输出fmt.Sprintf("%.2f", .表示的数据)
的返回字符串相同。
Go 模板库内置了一些基础的函数,若是要实现更为复杂的功能,能够自定义函数。
func formateDate(t time.Time) string {
return t.Format("2006-01-02")
}
func funcsHandler(w http.ResponseWriter, r *http.Request) {
funcMap := template.FuncMap{ "fdate": formateDate }
t := template.Must(template.New("funcs.html").Funcs(funcMap).ParseFiles("funcs.html"))
t.Execute(w, time.Now())
}
mux.HandleFunc("/funcs", funcsHandler)复制代码
模板文件funcs.html
:
<div>Today is {{ . | fdate }}</div>复制代码
自定义函数能够接受任意多个参数,可是只能返回一个值,或者返回一个值和一个错误。上面代码中,咱们必须先经过template.New
建立模板,而后调用Funcs
设置自定义函数,最后再解析模板文件。由于模板文件中使用了fdate
,未设置以前会解析失败。
上下文感知是html/template
库的一个很是有趣的特性。根据须要替换的文本在文档中所处的位置,模板在显示这些内容的时候会对其进行相应的修改。上下文感知的一个常见用途就是对内容进行转义。若是须要显示的是 HTML 的内容,那么进行 HTML 转义。若是显示的是 JavaScript 内容,那么进行 JavaScript 转义。Go 模板引擎还能识别出内容中的 URL 或 CSS,能够对它们实施正确的转义。
func contextAwareHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("context-aware.html"))
t.Execute(w, `He saied: <i>"She's alone?"</i>`)
}
mux.HandleFunc("/contextAware", contextAwareHandler)复制代码
模板文件context-aware.html
:
<div>{{ . }}</div>
<div><a href="/{{ . }}">Path</a></div>
<div><a href="/?q={{ . }}">Query</a></div>
<div><a onclick="f('{{ . }}')">JavaScript</a></div>复制代码
编译、运行程序,使用 curl 访问localhost:8080/contextAware
,获得下面的内容:
<!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>
<div>He saied: <i>"She's alone?"</i></div>
<div><a href="/He%20saied:%20%3ci%3e%22She%27s%20alone?%22%3c/i%3e">Path</a></div>
<div><a href="/?q=He%20saied%3a%20%3ci%3e%22She%27s%20alone%3f%22%3c%2fi%3e">Query</a></div>
<div><a onclick="f('He saied: \x3ci\x3e\x22She\x27s alone?\x22\x3c\/i\x3e')">JavaScript</a></div>
</body>
</html>复制代码
咱们依次来看,须要呈现的数据是He saied: "She's alone?"
:
div
中,直接在页面中显示,其中 HTML 标签
和单、双引号都被转义了; div
中,数据出如今 URL 的路径中,全部非法的路径字符都被转义了,包括空格、尖括号、单双引号; div
中,数据出如今查询字符串中,除了 URL 路径中非法的字符,还有冒号(:
)、问号(?
)和斜杠也被转义了; div
中,数据出如今 OnClick 代码中,单双引号和斜杠都被转义了。 这四种转义方式又有所不一样,第一种转义为 HTML 字符实体,第2、三种转义为 URL 转义字符(%
后跟字符编码的十六进制表示) ,第四种转义为 Go 中的十六进制字符表示。
XSS 是一种常见的攻击形式。在论坛之类的能够接受用户输入的网站,攻击者能够内容中添加
标签。若是网站未对输入的内容进行处理,其余用户浏览该页面时,:
点击 Submit 按钮:
alert
代码并无执行,为何?
咱们以前说过 Go 模板有上下文感知的功能,它检测到在 HTML 页面中,因此输入数据会被转义。查看网页源码能够看到转义后的结果:
那么如何才能不转义呢?html/template
提供了HTML
类型,Go 模板不会对该类型的变量进行转义。若是咱们把上面的处理器修改成:
func xssHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
t := template.Must(template.ParseFiles("xss-display.html"))
t.Execute(w, template.HTML(r.FormValue("comment")))
} else {
t := template.Must(template.ParseFiles("xss-form.html"))
t.Execute(w, nil)
}
}复制代码
再次实验,提交上面的内容,在 Chrome 浏览器中显示:
这意味着攻击者可让网站上的其余用户执行任意可能的攻击代码。
上一篇文章中说过,嵌套模板在定义网页布局时很是有用。
type NestInfo struct {
Name string
Todos []string
}
func nestHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("layout.html"))
data := NestInfo {
"dj", []string{"Homework", "Game", "Cleaning"},
}
t.ExecuteTemplate(w, "layout", data)
}复制代码
模板文件layout.html
:
{{ define "layout" }}
<!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>
{{ template "header" . }}
{{ template "content" . }}
{{ template "footer" . }}
</body>
</html>
{{ end }}
{{ define "header" }}
<h1>Hi, {{ .Name }}!</h1>
{{ end }}
{{ define "content" }}
Todo:
<ul>
{{ range .Todos }}
<li>{{ . }}</li>
{{ end }}
</ul>
{{ end }}
{{ define "footer" }}
Copyright © 2020 darjun.
{{ end }}复制代码
咱们将网页主体拆为三个部分:header、content和footer。header 通常显示导航栏,欢迎信息。content 展现主体内容。footer 中显示版权,联系方式等信息。
解析以后,咱们须要调用ExecuteTemplate
执行layout
模板,不能直接使用Execute
,由于主模板中只是定义了一些模板,没有具体的内容。
在上面的layout.html
文件中,咱们定义了模板layout
,layout
中使用了在本模板文件中定义的三个模板header/content/footer
。其实这几个模板的定义和使用能够经过block
动做合并在一块儿:
{{ define "layout" }}
<!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>
{{ block "header" . }}
<h1>Hi, {{ .Name }}!</h1>
{{ end }}
{{ block "content" . }}
Todo:
<ul>
{{ range .Todos }}
<li>{{ . }}</li>
{{ end }}
</ul>
{{ end }}
{{ block "footer" . }}
Copyright © 2020 darjun.
{{ end }}
</body>
</html>复制代码
block
至关于定义模板后当即使用。
{{ block "name" arg }}
...
{{ end }}复制代码
等价于:
{{ define "name" }}
...
{{ end }}
{{ template "name" arg }}复制代码
本文详细介绍了html/template
库,更多地经过案例来介绍,关注如何使用。模板在 Web 开发中是很是重要的部件,须要咱们紧紧掌握。
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~
本文由博客一文多发平台 OpenWrite 发布!