前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go text模版和HTML模版【Go语言圣经笔记】

Go text模版和HTML模版【Go语言圣经笔记】

作者头像
Steve Wang
发布2021-12-06 16:21:23
1.6K0
发布2021-12-06 16:21:23
举报
文章被收录于专栏:从流域到海域

text模版和HTML模版

如果只是最简单的格式化,使用Printf是完全足够的。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。这些功能是由text/templatehtml/template等模板包提供的,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。

一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象。大部分的字符串只是按字面值打印,但是对于actions部分将触发其它的行为。每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串:

代码语言:javascript
复制
import "text/tempalte"

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

这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间。对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被初始化为调用模板时的参数,在当前例子中对应github.IssuesSearchResult类型的变量。模板中{{.TotalCount}}对应action将展开为结构体中TotalCount成员以默认的方式打印的值。模板中{{range .Items}}和{{end}}对应一个循环action,因此它们直接的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。

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

代码语言:javascript
复制
func daysAgo(t time.Time) int {
    return int(time.Since(t).Hours() / 24)
}

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

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

代码语言:javascript
复制
report, err := template.New("report").
    Funcs(template.FuncMap{"daysAgo"}).  // 注册函数到模版中
    Parse(templ)
    
if err != nil {
    log.Fatal(err)    
}

模板通常在编译时测试完成,如果模板解析失败将是一个致命的错误。template.Must辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。

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

代码语言:javascript
复制
var report = template.Must(template.New("issueslist")).
    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)
    }
}

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

代码语言:javascript
复制
$ 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包相同的API和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和HTML、JavaScript、CSS或URL语法产生冲突的问题这个特性还可以避免一些长期存在的安全问题,比如通过生成HTML注入攻击,通过构造一个含有恶意代码的问题header,这些都可能让模板输出错误的信息,进而让他恶意们掌控页面。

下面的模板以HTML格式输出issue列表。注意import语句的不同:

代码语言:javascript
复制
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>
`))

下面的命令将在新的模板上执行一个稍微不同的查询:

代码语言:javascript
复制
$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html

下图显示了在web浏览器中的效果图。每个issue包含到Github对应页面的链接。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tsx2fS5W-1629943175244)(evernotecid://B80A85D8-91F7-47C1-AB3E-714AFC7C2671/appyinxiangcom/33702487/ENResource/p750)] 上图中issue没有包含会对HTML格式产生冲突的特殊字符,但是我们马上将看到标题中含有&和<字符的issue。下面的命令选择了两个这样的issue: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPG7cia1-1629943175247)(evernotecid://B80A85D8-91F7-47C1-AB3E-714AFC7C2671/appyinxiangcom/33702487/ENResource/p751)] 上图显示了该查询的结果。注意,html/template包已经自动将特殊字符转义,因此我们依然可以看到正确的字面值。如果我们使用text/template包的话,这2个issue将会产生错误,其中“<”四个字符将会被当作小于字符“<”处理,同时“”字符串将会被当作一个链接元素处理,它们都会导致HTML文档结构的改变,从而导致有未知的风险。

我们也可以通过对信任的HTML字符串使用template.HTML类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的JavaScript、CSS和URL。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果:A是一个普通字符串,B是一个信任的template.HTML字符串类型。

代码语言:javascript
复制
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        // untrust plain text
        B template.HTML  // trusted HTML
    }
    data.A = "<b>Hello!</b>"
    data.B = "<b>Hello!</b>"
    if err := t.Excute(os.Stdout, data); err != nil {
        log.Fatal(err)
    }
}

下图显示了出现在浏览器中的模板输出。我们看到A的黑体标记被转义失效了,但是B没有。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0NXqbU1-1629943175249)(evernotecid://B80A85D8-91F7-47C1-AB3E-714AFC7C2671/appyinxiangcom/33702487/ENResource/p752)] 我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:

代码语言:javascript
复制
$ go doc text/template
$ go doc html/template
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/08/26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • text模版和HTML模版
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档