文本和HTML模板
前面的例子,只是最简单的格式化,使用 Printf 是完全足够的。但是有时候会需要复杂的打印格式,这时候一般需要将格式化代码分离出来以便更安全地修改。这些功能是由 text/template 和 html/template 等模板包提供的,它们提供了一个将变量值填充到一个文本或 HTML 格式的模板的机制。
一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的 {{action}}
对象。大部分的字符串只是按字面值打印,但是对于 actions 部分将触发其它的行为。每个 actions 都包含了一个用模板语言书写的表达式,一个 action 虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流 if-else 语句和 range 循环语句,还有其它实例化模板等诸多特性。下面是一个简单的模板字符串:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/issuesreport/main.go[]
这个模板先打印匹配到的 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 成员转换为过去的时间长度:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/issuesreport/main.go[]
需要注意的是 CreatedAt 的参数类型是 time.Time ,并不是字符串。以同样的方式,我们可以通过定义一些方法来控制字符串的格式化(§2.5),一个类型同样可以定制自己的 JSON 编码和解码行为。 time.Time 类型对应的 JSON 值是一个标准时间格式的字符串。
生成模板的输出需要两个处理步骤。第一步是要分析模板并转为内部表示,然后基于指定的输入执行模板。分析模板部分一般只需要执行一次。下面的代码创建并分析上面定义的模板 templ 。注意方法调用链的顺序: template.New 先创建并返回一个模板;Funcs 方法将 daysAgo 等自定义函数注册到模板中,并返回模板;最后调用 Parse 函数分析模板。
Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/issuesreport/main.go[]
因为模板通常在编译时就测试好了,如果模板解析失败将是一个致命的错误。template.Must 辅助函数可以简化这个致命错误的处理:它接受一个模板和一个 error 类型的参数,检测 error 是否为 nil(如果不是 nil 则发出 panic 异常),然后返回传入的模板。我们将在 5.9 节再讨论这个话题。
一旦模板已经创建、注册了 daysAgo 函数、并通过分析和检测,我们就可以使用 github.IssuesSearchResult 作为输入源、os.Stdout 作为输出源来执行模板:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/issuesreport/main.go[]
程序输出一个纯文本报告:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/issuesreport/main.go[]
现在让我们转到 html/template 模板包。它使用和 text/template 包相同的 API 和模板语言,但是增加了一个将字符串自动转义特性,这可以避免输入字符串和 HTML、JavaScript、CSS 或 URL 语法产生冲突的问题。这个特性还可以避免一些长期存在的安全问题,比如通过生成 HTML 注入攻击,通过构造一个含有恶意代码的问题标题,这些都可能让模板输出错误的输出,从而让他们控制页面。
下面的模板以 HTML 格式输出 issue 列表。注意 import 语句的不同:
Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/issueshtml/main.go[]
下面的命令将在新的模板上执行一个稍微不同的查询:
$ go build gopl.io/ch4/issueshtml
$ ./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html
图4.4显示了在 web 浏览器中的效果图。每个 issue 包含到 Github 对应页面的链接。

图4.4中 issue 没有包含会对 HTML 格式产生冲突的特殊字符,但是我们马上将看到标题中含有 &
和 <
字符的 issue 。下面的命令选择了两个这样的 issue :
$ ./issueshtml repo:golang/go 3133 10535 >issues2.html
图4.5显示了该查询的结果。注意,html/template 包已经自动将特殊字符转义,因此我们依然可以看到正确的字面值。如果我们使用 text/template 包的话,这 2 个 issue 将会产生错误,其中 <
四个字符将会被当作小于字符 <
处理,同时 <link>
字符串将会被当作一个链接元素处理,它们都会导致 HTML 文档结构的改变,从而导致有未知的风险。
我们也可以通过对信任的 HTML 字符串使用 template.HTML 类型来抑制这种自动转义的行为。还有很多采用类型命名的字符串类型分别对应信任的 JavaScript、CSS 和 URL 。下面的程序演示了两个使用不同类型的相同字符串产生的不同结果:A 是一个普通字符串,B 是一个信任的 template.HTML 字符串类型。

Unresolved include directive in modules/ROOT/pages/ch4/ch4-06.adoc - include::example$/ch4/autoescape/main.go[]
图4.6显示了出现在浏览器中的模板输出。我们看到 A 的黑体标记被转义失效了,但是 B 没有。
我们这里只讲述了模板系统中最基本的特性。一如既往,如果想了解更多的信息,请自己查看包文档:
$ go doc text/template
$ go doc html/template
练习 4.14: 创建一个 web 服务器,查询一次 GitHub,然后生成 BUG 报告、里程碑和对应的用户信息。