Web页面的展示依靠的是模板技术,Web模板就是一些预先设计好的 HTML 页面,模板引擎会通过重复地使用这些页面来创建一个或多个 HTML 页面。

1. Go的模板引擎

Go 语言的模板引擎也是介于无逻辑模板引擎嵌入逻辑模板引擎之间的一种模板引擎。

Go 的模板都是文本文档(其中 Web 应用的模板通常都是 HTML ),它们都嵌入了一些称为动作( action )的指令。 从模板引擎的角度来说,模板就是嵌入了动作的文本,而模板引擎则通过分析并执行这些文本来生成出另外一些文本

1.1 模板的使用过程

模板中的动作默认使用两个大括号包围。下面就是一个简单的模板:

<!DOCTYPE html>
<html>  
    <head>    
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">    
        <title>Go Web Programming</title>
    </head>
    <body>    
        {{ . }}  
    </body>
</html>

代码的模板来源于一个名为 tmpl.html 的模板文件。用户可以拥有任意多个模板文件, 并且这些模板文件可以使用任意后缀名,但它们的类型必须是可读的文本格式。 因为 上面这段模板的输出将是一个HTML文件,所以我们使用了.html作为模板文件的后缀名。

func process(w http.ResponseWriter, r *http.Request){
    t,_:=template.ParseFiles("tmpl.html")
    t.Execute(w,"hello world")
}
func main(){
    server:=http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/process",process)
    server.ListenAndServe()
}

上面的代码展示了使用模板的两个关键步骤:

  • 调用模板引擎分析模板
  • 将数据填入模板引擎,充实动作

ResponseWriter 和数据会一起被传入 Execute 方法中,这样一来,模板引擎在生成 HTML 之后就可以把该HTML文件传给 ResponseWriter了。

1.2 对模板进行语法分析

当用户调用 ParseFiles 函数的时候, Go会创建一个新的模板,并将用户给定的模板文件的名字用作这个新模板的名字:

t,_ :=template.ParseFiles("tmpl.html")

//等价于
t := template.New("tmpl.html")
t,_ := t.ParseFiles("tmpl.html")

ParseFiles函数可以接受任意数量的文件作为参数,会将他们合并视为一个模板集合

我们也可以使用ParseGlob进行全局模板分析,读取所有html文件。

t,_ := template.ParseGlob("*.html")

1.3 执行模板

但如果模板不止一个,那么当对模板集合调用 Execute 方法的时候, Execute方法只会执行模板集合中的第一个模板。

t, _ := template.ParseFiles("t1.html","t2.html")
t.Execute(w,"Hello!")

如果想执行另一个,就需要使用函数ExecuteTemplate

t.ExecuteTemplate(w, "t2.html","Hello")

2. 动作

2.1 条件动作

条件动作会根据参数的值来决定对多条语句中的哪一条语句进行求值。最简单的条件动作的格式如下:

{{ if arg }}
  some content
{{ end }}

这个动作的另一种格式如下:

{{ if arg }}
some content
{{ else }}
other content
{{ end }}

下面的Demo中,我们会在服务器上面创建一个处理器,这个处理器会随机0-10之间的整数,然后通过判断这个随机整数是否大于5。

func process(w http.ResponseWriter, r *http.Request) {    
    t, _ := template.ParseFiles("tmpl.html")    
    rand.Seed(time.Now().Unix())    
    t.Execute(w, rand.Intn(10) > 5)
}

将模板文件的body改为:

<body>    
    {{ if . }}      
    Number is greater than 5!    
    {{ else }}      
    Number is 5 or less!   
    {{ end }}  
</body>

2.2 迭代动作

迭代动作可以对数组、切片、映射或者通道进行迭代,而在迭代循环的内部,.则会被设置为当前被迭代的元素,就像这样:

{{ range array }}
    Dot is set to the element {{ . }}
{{ end }}

下面是一个使用了迭代动作的例子:

<body>
    <ul>
        {{ range . }}
        <li>{{ . }}</li>
        {{ end}}
    </ul>
</body>

下面是模板处理器:

func process(w http.ResponseWriter, r *http.Request) {    
    t, _ := template.ParseFiles("tmpl.html")    
    daysOfWeek := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}    
    t.Execute(w, daysOfWeek)
}

在网页中,会以无序列表的形式展示周一到周日:


下面展示了一个具有备选结果的迭代动作:

<body>    
    <ul>    
        {{ range . }}      
        <li>{{ . }}</li>  
        {{ else }}      
        <li> Nothing to show </li>    
        {{ end}}    
    </ul>  
</body>

2.3 设置动作

设置动作允许用户在指定的范围之内为点.设置值。比如,在以下代码中:

{{ with arg }}
    Dot is set to arg
{{ end }}

介于with argend之间的点将被设置为参数arg的值,比如:

<body>    
    <div>The dot is {{ . }}</div>
    <div>    
        {{ with "world"}}      
        Now the dot is set to {{ . }}    
        {{ end }}    
    </div>    
    <div>The dot is {{ . }} again</div>  
</body>

传入的还是hello,但是经过设置动作后,点的内容被替换为world。

2.4 包含动作

包含动作(include action)允许用户在一个模板里面包含另一个模板,从而构建出嵌套的模板。包含动作的格式为template "name",其中name参数为被包含模板的名字。

<body>    
    <div> This is t1.html before</div>    
    <div>This is the value of the dot in t1.html - [{{ . }}]</div>    
    <hr/>    
    {{ template "t2.html" }}    
    <hr/>    
    <div> This is t1.html after</div>
</body>
<div style="background-color: yellow;">  
    This is t2.html<br/>  
    This is the value of the dot in t2.html - [{{ . }}]
</div>

下面展示了处理器:

func process(w http.ResponseWriter, r *http.Request) {    
    t, _ := template.ParseFiles("t1.html", "t2.html")    
    t.Execute(w, "Hello World!")
}

结果如下:

3. 函数、变量与管道

3.1 变量与管道

用户还可以在动作中设置变量。变量以美元符号$开头,就像这样:

$variable := value

利用变量,我们可以实现迭代动作的一个变种:

{{ range $key, $value := . }}  
    The key is {{ $key }} and the value is {{ $value }}
{{ end }}

模板中的管道(pipeline)是多个有序地串联起来的参数、函数和方法,它的工作方式和语法跟Unix的管道也非常相似:

{{ p1 | p2 | p3 }}

管道允许用户将一个参数的输出传递给下一个参数,而各个参数之间则使用|分隔。

<body>    
    {{ 12.3456 | printf "%.2f" }}  
</body>

上面的代码就是一个利用管道原理格式化的例子。

3.2 函数

Go函数也可以用作模板的参数:Go模板引擎内置了一些非常基础的函数。需要注意的是,Go的模板引擎函数都是受限制的:尽管这些函数可以接受任意多个参数作为输入,但它们只能返回一个值,或者返回一个值和一个错误。

为了创建一个自定义模板函数,用户需要:

  1. 创建一个名为FuncMap的映射,并将映射的键设置为函数的名字,而映射的值则设置为实际定义的函数;
  2. FuncMap与模板进行绑定。

举个例子:在编写Web应用的时候,用户常常需要将时间对象或者日期对象转换为ISO8601格式的时间字符串或者日期字符串,又或者将ISO8601格式的字符串转换为相应的对象。

func formatDate(t time.Time) string {    
    layout := "2006-01-02"    
    return t.Format(layout)
}

func process(w http.ResponseWriter, r *http.Request) {    
    funcMap := template.FuncMap { "fdate": formatDate }    
    t := template.New("tmpl.html").Funcs(funcMap)    
    t, _ = t.ParseFiles("tmpl.html")    
    t.Execute(w, time.Now())
}
  1. 首先定义了一个名为formatDate的函数,它接受一个Time结构作为输入,然后以年-月-日的形式返回一个ISO8601格式的字符串。
  2. 在之后的处理器中,程序将名字fdate映射至formatDate函数。
  3. 序使用template.New函数创建了一个名为tmpl.html的模板。以程序直接以串联的方式调用模板的Funcs方法。

在html文件中,通过管道使用自定义函数:

<body>    
    <div>The date/time is {{ . | fdate }}</div> 
</body>

4. 上下文感知

4.1 什么是上下文感知

所谓上下文感知就是对被显示的内容实施正确的转义:如果模板显示的是HTML格式的内容,那么模板将对其实施HTML转义;如果模板显示的是JavaScript格式的内容,那么模板将对其实施JavaScript转义;诸如此类。除此之外,Go模板引擎还可以识别出内容中的URL或者CSS样式。

比如:

t, _ := template.ParseFiles("tmpl.html")    
content := `I asked: <i>"What's up?"</i>`
t.Execute(w, content)

上下文感知模板:

 <body>    
     <div>{{ . }}</div>    
     <div><a href="/{{ . }}">Path</a></div>    
     <div><a href="/?q={{ . }}">Query</a></div> 
     <div><a onclick="f('{{ . }}')">Onclick</a></div>  
 </body>

根据动作所在位置,输出结果将变为:

4.2 防御XSS攻击

XXS也称 cross-site scripting,跨站脚本。这种攻击是由于服务器将攻击者存储的数据原原本本地显示给其他用户所致的

举个例子,如果有一个存在持久性 xss 漏洞的论坛,它允许用户在论坛上面发布帖子或者回复,并且其他用户也可以阅读这些帖子以及回复,那么攻击者就可能会在他发布的内容中引入带有<script>标签的代码。 因为论坛即使在内容带有<script>标签的情况下,仍然会原原本本地向用户显示这些内容,所以用户将在毫不知情的情况下,使用自己的权限去执行攻击者发布的恶意代码。 预防这一攻击的常见方法就是在显示或者存储用户传入的数据之前,对数据进行转义。

假设我们想要通过一个HTML表单发送数据:

<html>  
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">    
    <title>Go Web Programming</title>  
    </head>  
    <body>    
        <form action="/process" method="post">      
            Comment: <input name="comment" type="text">
            <hr/>     
            <button id="submit">Submit</button> 
        </form>  
    </body>
</html>

为了防止XSS攻击,我们使用如下模板:

<html>  
    <head>    
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">    
        <title>Go Web Programming</title>  
    </head>  

    <body>    
        <div>{{ . }}</div>  
    </body>
</html>

然后将模板和表单都绑定到处理器:

http.HandleFunc("/process", process)    
http.HandleFunc("/form", form)

当我们访问form时,将以下内容输入到表单的文本框里面,然后按下Submit按钮:

<script>alert('Pwnd!');</script>

由于go的模板引擎的防护,将漏洞成功转义:

4.3 不转义

如果真的想要允许用户输入HTML代码或者JavaScript代码,并在显示内容时执行这些代码,可以使用Go提供的“不转义HTML”机制:只要把不想被转义的内容传给template.HTML函数,模板引擎就不会对其进行转义。

func process(w http.ResponseWriter, r *http.Request) {    
    t, _ := template.ParseFiles("tmpl.html")    
    t.Execute(w, template.HTML(r.FormValue("comment")))
}