Go语言核心之美 3.6-template模版

erlib · · 854 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

上一节的例子中使用了Printf做了输出格式化,当然,对于简单的例子来说足够了,但是我们有时候还是需要复杂的输出格式,甚至需要将格式化代码分离开来。这时,可以使用text/template和html/template。

一个模版就是一个字符串或者一个文件,里面包含了一个或多个{{..}}形式的对象,这种对象被称为模版的actions。除了actions外,模版的其它部分就按字面值打印,但是对于actions,将触发相应的行为。每个action都是一个模版表达式,模版表达式包含:选择struct中的字段、调用函数或方法、if-else控制语句和range循环语句等等。下面是一个简单的模版:

gopl.io/ch4/issuesreport

const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User:   {{.User.Login}}
Title:  {{.Title | printf "%.64s"}}
Age:    {{.CreatedAt | daysAgo}} days
{{end}}`

让我们继续上节中获取github上的issuses项目。上面这个模版先打印匹配到的issues总数,然后打印每个issue的编号、用户、标题以及存在的时间。对于每一个action,都可以用点操作符"."代表当前的值。点操作符首先初始化为模版的参数(模版初始化时会传入参数),当前例子中对应github.IssuesSearchResult,{{.TotalCount}} action将被struct中的TotalCount字段值代替并打印。{{range .Items}}和{{end}}这两个action合在一起对应一个循环,循环中的点操作符代表的是当前的Items元素值,因此如果循环n次,那么就会打印n次Item中的元素。

在action中, |  操作符表示将前一个表达式的结果用作后一个函数的输入,这个类似于Unix管道。在Title这一行的action中,第二个表达式是printf函数,这个函数是基于fmt.Sprintf实现的内置函数,所有模版都可以直接使用。对于Age这一行,|  后面是一个daysAgo函数,该函数通过time.Since将CreatedAt转换为时间长度:

func daysAgo(t time.Time) int {
    return int(time.Since(t).Hours() / 24)
}

要注意的是CreatedAt的参数类型是time.Time,并不是字符串,我们可以通过定义一些methods来控制字符串的格式化,一个类型同样可以通过自定义method来控制JSON的编码和解码行为。time.Time类型对应的JSON值是一个标准时间格式的字符串。

生成输出模版需要两个步骤:第一步是分析模版并转化为内部表示,第二步是基于指定的输入来执行模版,分析模版只需要执行一次即可。下面的代码创建并分析上面定义的模版templ,注意函数调用链的顺序:template.New先创建并返回一个模版;Funcs方法将daysAgo等自定义函数注册到模版中,并返回模版;最后调用Parse函数分析模版:

report, err := template.New("report").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ)
if err != nil {
    log.Fatal(err)
}

因为模版在编译时就会确定,且模版的解析失败是一个致命的错误,我们还可以使用template.Must函数来简化错误处理:函数接收一个模版和error参数,然后在处理完模版后检测error是否为nil(如果不是nil直接抛出panic异常),最后返回处理完的模版。我们将在4.9节再讨论这个话题。

一旦模版创建、注册daysAgo函数、并通过了分析和检测,我们就可以使用github.IssuesSearchResult做为输入,os.Stdout做为输出来执行模版:

var report = template.Must(template.New("issuelist").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ))

func main() {
    result, err := github.SearchIssues(os.Args[1:])
    if err != nil {
        log.Fatal(err)
    }
    if err := report.Execute(os.Stdout, result); err != nil {
        log.Fatal(err)
    }
}

程序输出一个纯文本报告:

$ go build gopl.io/ch4/issuesreport
$ ./issuesreport repo:golang/go is:open json decoder
13 issues:
----------------------------------------
Number: 5680
User:      eaigner
Title:     encoding/json: set key converter on en/decoder
Age:       750 days
----------------------------------------
Number: 6050
User:      gopherbot
Title:     encoding/json: provide tokenizer
Age:       695 days
----------------------------------------
...

现在让我们来看看html/template包,它的使用方法和text/template相同,但是增加了将HTML、Javascript、CSS、URL中的字符串自动转义的特性,这样可以避免生成HTML时使用的注入攻击技术(js注入攻击),恶意的对手通过构造一个含有恶意代码的issue title,让模版产生错误的输出,进而控制页面。

下面的模版通过HTML格式输出issue列表,注意import语句的不同:

gopl.io/ch4/issueshtml

import "html/template"

var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
  <th>#</th>
  <th>State</th>
  <th>User</th>
  <th>Title</th>
</tr>
{{range .Items}}
<tr>
  <td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
  <td>{{.State}}</td>
  <td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
  <td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))

在新的模版上执行一次查询:

$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html

图4.4显示了在web浏览器中的效果图。每个issue都包含了跳转到Github对应页面的链接。

图4.4显示的HTML没有什么问题字符,但是下面看到的issue的标题中将包含&和<字符:

$ ./issueshtml repo:golang/go 3133 10535 >issues2.html

图4.5显示了本次查询的结果。注意,html/template已经自动将特殊字符进行了转义,因此我们依然可以看到正确的字面值。如果上面错误的使用了text/template替代了html/template,那"&lt"会被当作"<"处理,"<link>"被当作link元素处理,这些都会导致HTML文档的改变,从而导致潜在的风险。

我们也可以使用template.HTML类型来抑制这种自动转义的行为,同时还有其它类似的类型对应JavaScript、CSS、URL。下面的程序演示了两个使用不同类型来处理相同字符串:

gopl.io/ch4/autoescape

func main() {
    const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
    t := template.Must(template.New("escape").Parse(templ))
    var data struct {
        A string        // untrusted plain text
        B template.HTML // trusted HTML
    }
    data.A = "<b>Hello!</b>"
    data.B = "<b>Hello!</b>"
    if err := t.Execute(os.Stdout, data); err != nil {
        log.Fatal(err)
    }
}

图4.6显示了浏览器中的模板输出,其中A的黑体显示因为转义失效了,B没有:

我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:

$ go doc text/template
$ go doc html/template

练习 4.14: 创建一个web服务器,查询一次GitHub,然后生成BUG报告、里程碑(milestones)和对应的用户信息。


有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:erlib

查看原文:Go语言核心之美 3.6-template模版

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

854 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传