package template

import "html/template"

Package template (html/template) 实现了数据驱动的模板,用于生成防止代码注入的安全 HTML 输出。 它提供与 text/template 相同的接口,在输出为 HTML 时应使用本包代替 text/template

本文档重点介绍本包的安全特性。有关如何编写模板本身的信息,请参阅 text/template 的文档。

简介

本包封装了 text/template,使你可以共享其模板 API 来安全地解析和执行 HTML 模板。

tmpl, err := template.New("name").Parse(...)
// 省略错误检查
err = tmpl.Execute(out, data)

如果成功,tmpl 将是注入安全的。否则,err 是 ErrorCode 文档中定义的错误。

HTML 模板将数据值视为纯文本,这些纯文本会被编码以便安全地嵌入 HTML 文档中。 转义是上下文相关的,因此 action 可以出现在 JavaScript、CSS 和 URI 上下文中。

注释会从输出中被移除,但通过 HTMLCSSJS 类型在各自上下文中传入的除外。

本包使用的安全模型假定模板作者是可信的,而 Execute 的 data 参数是不可信的。 更多细节见下文。

示例

import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

产生

Hello, <script>alert('you have been pwned')</script>!

但 html/template 中的上下文自动转义

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

产生安全的、转义后的 HTML 输出

Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!

上下文

本包理解 HTML、CSS、JavaScript 和 URI。它会为每个简单的 action 管道添加 净化函数,因此给定以下片段

<a href="/search?q={{.}}">{{.}}</a>

在解析时,每个 {{.}} 都会被重写以根据需要添加转义函数。在本例中它变为

<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>

其中 urlescaper、attrescaper 和 htmlescaper 是内部转义函数的别名。

对于这些内部转义函数,如果 action 管道的求值结果是 nil 接口值, 则将其视为空字符串。

带命名空间和 data- 属性

带有命名空间的属性被视为没有命名空间。给定以下片段

<a my:href="{{.}}"></a>

在解析时,该属性将被视为只是 "href"。因此在解析时模板变为:

<a my:href="{{. | urlescaper | attrescaper}}"></a>

类似于带命名空间的属性,带有 "data-" 前缀的属性被视为没有 "data-" 前缀。因此给定

<a data-href="{{.}}"></a>

在解析时变为

<a data-href="{{. | urlescaper | attrescaper}}"></a>

如果属性同时具有命名空间和 "data-" 前缀,在确定上下文时只会移除命名空间。例如

<a my:data-href="{{.}}"></a>

这会被处理为 "my:data-href" 只是 "data-href" 而不是 "href", 因为如果 "data-" 前缀也被忽略就会变成 "href"。因此在解析时只变为

<a my:data-href="{{. | attrescaper}}"></a>

作为特殊情况,具有 "xmlns" 命名空间的属性始终被视为包含 URL。给定以下片段

<a xmlns:title="{{.}}"></a>
<a xmlns:href="{{.}}"></a>
<a xmlns:onclick="{{.}}"></a>

在解析时变为:

<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>

错误

详情请参阅 ErrorCode 的文档。

更完整的图景

本包注释的其余部分在首次阅读时可以跳过;它包含理解转义上下文和错误消息 所需的细节。大多数用户不需要了解这些细节。

上下文

假设 {{.}} 是 `O'Reilly: How are <i>you</i>?`,下表显示了 {{.}} 在左侧上下文中 使用时的显示方式。

上下文                            {{.}} 之后
{{.}}                            O'Reilly: How are &lt;i&gt;you&lt;/i&gt;?
<a title='{{.}}'>                O&#39;Reilly: How are you?
<a href="/{{.}}">                O&#39;Reilly: How are %3ci%3eyou%3c/i%3e?
<a href="?q={{.}}">              O&#39;Reilly%3a%20How%20are%3ci%3e...%3f
<a onx='f("{{.}}")'>             O\x27Reilly: How are \x3ci\x3eyou...?
<a onx='f({{.}})'>               "O\x27Reilly: How are \x3ci\x3eyou...?"
<a onx='pattern = /{{.}}/;'>     O\x27Reilly: How are \x3ci\x3eyou...\x3f

如果在不安全的上下文中使用,则该值可能会被过滤掉:

上下文                            {{.}} 之后
<a href="{{.}}">                 #ZgotmplZ

因为 "O'Reilly:" 不是像 "http:" 那样被允许的协议。

如果 {{.}} 是无害的单词 `left`,则它可以更广泛地出现,

上下文                                {{.}} 之后
{{.}}                                left
<a title='{{.}}'>                    left
<a href='{{.}}'>                     left
<a href='/{{.}}'>                    left
<a href='?dir={{.}}'>                left
<a style="border-{{.}}: 4px">        left
<a style="align: {{.}}">             left
<a style="background: '{{.}}'>       left
<a style="background: url('{{.}}')>  left
<style>p.{{.}} {color:red}</style>   left

非字符串值可以在 JavaScript 上下文中使用。如果 {{.}} 是

struct{A,B string}{ "foo", "bar" }

在转义后的模板中

<script>var pair = {{.}};</script>

则模板输出为

<script>var pair = {"A": "foo", "B": "bar"};</script>

请参阅 json 包以了解非字符串内容如何被序列化以嵌入 JavaScript 上下文。

类型化字符串

默认情况下,本包假定所有管道产生纯文本字符串。 它会添加必要的转义管道阶段,以正确安全地将该纯文本字符串嵌入到适当的上下文中。

当数据值不是纯文本时,你可以通过标记其类型来确保它不会被过度转义。

HTML、JS、URL 以及 content.go 中的其他类型可以承载安全内容,这些内容可豁免转义。

模板

Hello, {{.}}!

可以通过以下方式调用

tmpl.Execute(out, template.HTML(`<b>World</b>`))

以产生

Hello, <b>World</b>!

而不是

Hello, &lt;b&gt;World&lt;b&gt;!

如果 {{.}} 是普通字符串则会产生后者。

安全模型

https://web.archive.org/web/20160501113828/http://js-quasis-libraries-and-repl.googlecode.com/svn/trunk/safetemplate.html#problem_definition 定义了本包所使用的"安全"的含义。

本包假定模板作者是可信的,而 Execute 的 data 参数是不可信的, 并致力于在面对不可信数据时保持以下属性:

结构保持属性: "……当模板作者在安全的模板语言中编写 HTML 标签时, 浏览器会将输出的相应部分解释为标签, 无论不可信数据的值如何,其他结构(如属性边界、JS 和 CSS 字符串边界)也是如此。"

代码效果属性: "……只有模板作者指定的代码应该在将模板输出注入页面后运行, 并且模板作者指定的所有代码都应该因此而运行。"

最少意外属性: "一个熟悉 HTML、CSS 和 JavaScript 的开发者(或代码审查者), 知道会发生上下文自动转义,应该能够查看 {{.}} 并正确推断出会发生什么净化处理。"

之前,ECMAScript 6 模板字面量默认被禁用,可以通过 GODEBUG=jstmpllitinterp=1 环境变量启用。模板字面量现在默认被支持,设置 jstmpllitinterp 不再有任何效果。

Example
package main

import (
	"html/template"
	"log"
	"os"
)

func main() {
	const tpl = `
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>{{.Title}}</title>
	</head>
	<body>
		{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
	</body>
</html>`

	check := func(err error) {
		if err != nil {
			log.Fatal(err)
		}
	}
	t, err := template.New("webpage").Parse(tpl)
	check(err)

	data := struct {
		Title string
		Items []string
	}{
		Title: "My page",
		Items: []string{
			"My photos",
			"My blog",
		},
	}

	err = t.Execute(os.Stdout, data)
	check(err)

	noItems := struct {
		Title string
		Items []string
	}{
		Title: "My another page",
		Items: []string{},
	}

	err = t.Execute(os.Stdout, noItems)
	check(err)

}

Output:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>My page</title>
	</head>
	<body>
		<div>My photos</div><div>My blog</div>
	</body>
</html>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>My another page</title>
	</head>
	<body>
		<div><strong>no rows</strong></div>
	</body>
</html>
Example (Autoescaping)
package main

import (
	"html/template"
	"log"
	"os"
)

func main() {
	check := func(err error) {
		if err != nil {
			log.Fatal(err)
		}
	}
	t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
	check(err)
	err = t.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have been pwned')</script>")
	check(err)
}

Output:

Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
Example (Escape)
package main

import (
	"fmt"
	"html/template"
	"os"
)

func main() {
	const s = `"Fran & Freddie's Diner" <tasty@example.com>`
	v := []any{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`}

	fmt.Println(template.HTMLEscapeString(s))
	template.HTMLEscape(os.Stdout, []byte(s))
	fmt.Fprintln(os.Stdout, "")
	fmt.Println(template.HTMLEscaper(v...))

	fmt.Println(template.JSEscapeString(s))
	template.JSEscape(os.Stdout, []byte(s))
	fmt.Fprintln(os.Stdout, "")
	fmt.Println(template.JSEscaper(v...))

	fmt.Println(template.URLQueryEscaper(v...))

}

Output:

&#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
&#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
&#34;Fran &amp; Freddie&#39;s Diner&#34;32&lt;tasty@example.com&gt;
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
%22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E

Index

Examples

Functions

func HTMLEscape

func HTMLEscape(w io.Writer, b []byte)

HTMLEscape 将纯文本数据 b 的转义 HTML 等价物写入 w。

func HTMLEscapeString

func HTMLEscapeString(s string) string

HTMLEscapeString 返回纯文本数据 s 的转义 HTML 等价物。

func HTMLEscaper

func HTMLEscaper(args ...any) string

HTMLEscaper 返回其参数的文本表示的转义 HTML 等价物。

func IsTrue

func IsTrue(val any) (truth, ok bool)

IsTrue 报告值是否为"真"——即不是其类型的零值, 以及该值是否具有有意义的真值。这是 if 和其他类似 action 使用的真值定义。

func JSEscape

func JSEscape(w io.Writer, b []byte)

JSEscape 将纯文本数据 b 的转义 JavaScript 等价物写入 w。

func JSEscapeString

func JSEscapeString(s string) string

JSEscapeString 返回纯文本数据 s 的转义 JavaScript 等价物。

func JSEscaper

func JSEscaper(args ...any) string

JSEscaper 返回其参数的文本表示的转义 JavaScript 等价物。

func URLQueryEscaper

func URLQueryEscaper(args ...any) string

URLQueryEscaper 返回其参数的文本表示的转义值,格式适合嵌入 URL 查询。

Types

type CSS

type CSS string

CSS 封装了已知安全的内容,匹配以下任意一种:

  1. CSS3 样式表产生式,如 `p { color: purple }`。
  2. CSS3 规则产生式,如 `a[href=~"https:"].foo#bar`。
  3. CSS3 声明产生式,如 `color: red; margin: 2px`。
  4. CSS3 值产生式,如 `rgba(0, 0, 255, 127)`。

参见 https://www.w3.org/TR/css3-syntax/#parsinghttps://web.archive.org/web/20090211114933/http://w3.org/TR/css3-syntax#style

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。

type Error

type Error struct {
	// ErrorCode 描述错误的种类。
	ErrorCode ErrorCode
	// Node 是导致问题的节点(如果已知)。
	// 如果非 nil,它将覆盖 Name 和 Line。
	Node parse.Node
	// Name 是遇到错误的模板名称。
	Name string
	// Line 是模板源码中的错误行号,0 表示未知。
	Line int
	// Description 是对问题的可读描述。
	Description string
}

Error 描述模板转义过程中遇到的问题。

func (*Error) Error

func (e *Error) Error() string

type ErrorCode

type ErrorCode int

ErrorCode 是一种错误的错误码。

const (
	// OK 表示没有错误。
	OK ErrorCode = iota

	// ErrAmbigContext:"... 出现在 URL 中的歧义上下文中"
	// 示例:
	//   <a href="
	//      {{if .C}}
	//        /path/
	//      {{else}}
	//        /search?q=
	//      {{end}}
	//      {{.X}}
	//   ">
	// 讨论:
	//   {{.X}} 处于歧义的 URL 上下文中,因为根据 {{.C}} 的值,
	//   它可能是 URL 后缀或查询参数。
	//   将 {{.X}} 移入条件分支可消除歧义:
	//   <a href="{{if .C}}/path/{{.X}}{{else}}/search?q={{.X}}">
	ErrAmbigContext

	// ErrBadHTML:"期望空格、属性名或标签结束,但得到了……",
	//   "……在未加引号的属性中","……在属性名中"
	// 示例:
	//   <a href = /search?q=foo>
	//   <href=foo>
	//   <form na<e=...>
	//   <option selected<
	// 讨论:
	//   这通常是由 HTML 元素中的拼写错误引起的,但某些字符在标签名、
	//   属性名和未加引号的属性值中是被禁止的,因为它们可能引发解析器歧义。
	//   给所有属性加引号是最佳策略。
	ErrBadHTML

	// ErrBranchEnd:"{{if}} 分支在不同的上下文中结束"
	// 示例:
	//   {{if .C}}<a href="{{end}}{{.X}}
	//   <script {{with .T}}type="{{.}}"{{end}}>
	// 讨论:
	//   html/template 包静态地检查通过 {{if}}、{{range}} 或 {{with}} 的每条路径,
	//   以转义后续的管道。第一个示例是歧义的,因为 {{.X}} 可能是 HTML 文本节点,
	//   也可能是 HTML 属性中的 URL 前缀。{{.X}} 的上下文用于确定如何转义它,
	//   但该上下文取决于 {{.C}} 的运行时值,而这在静态分析时是未知的。
	//   第二个示例是歧义的,因为 script type 属性可以改变脚本内容所需的转义类型。
	//
	//   问题通常是缺少引号或尖括号之类的东西,或者可以通过重构来将两个上下文
	//   放入 if、range 或 with 的不同分支中来避免。如果问题出在对一个不应为空
	//   的集合进行 {{range}} 时,添加一个虚拟的 {{else}} 可能会有帮助。
	ErrBranchEnd

	// ErrEndContext:"……以非文本上下文结束:……"
	// 示例:
	//   <div
	//   <div title="no close quote>
	//   <script>f()
	// 讨论:
	//   执行的模板应产生 HTML 的 DocumentFragment。
	//   没有闭合标签就结束的模板将触发此错误。
	//   不应在 HTML 上下文中使用的模板或产生不完整 Fragment 的模板不应直接执行。
	//
	//   {{define "main"}} <script>{{template "helper"}}</script> {{end}}
	//   {{define "helper"}} document.write(' <div title=" ') {{end}}
	//
	//   "helper" 不产生有效的文档片段,因此不应直接执行。
	ErrEndContext

	// ErrNoSuchTemplate:"没有这样的模板……"
	// 示例:
	//   {{define "main"}}<div {{template "attrs"}}>{{end}}
	//   {{define "attrs"}}href="{{.URL}}"{{end}}
	// 讨论:
	//   html/template 包会查看模板调用来计算上下文。
	//   这里 "attrs" 中的 {{.URL}} 在从 "main" 调用时必须被视为 URL,
	//   但如果在解析 "main" 时 "attrs" 尚未定义,你将得到此错误。
	ErrNoSuchTemplate

	// ErrOutputContext:"无法计算模板的输出上下文……"
	// 示例:
	//   {{define "t"}}{{if .T}}{{template "t" .T}}{{end}}{{.H}}",{{end}}
	// 讨论:
	//   递归模板没有在其开始的相同上下文中结束,因此无法计算可靠的输出上下文。
	//   检查命名模板中是否有拼写错误。
	//   如果模板不应在命名的起始上下文中被调用,请检查是否在意外的上下文中调用了该模板。
	//   也许可以重构递归模板使其不再递归。
	ErrOutputContext

	// ErrPartialCharset:"未完成的 JS 正则表达式字符集……"
	// 示例:
	//     <script>var pattern = /foo[{{.Chars}}]/</script>
	// 讨论:
	//   html/template 包不支持在正则表达式字面量字符集中进行插值。
	ErrPartialCharset

	// ErrPartialEscape:"未完成的转义序列……"
	// 示例:
	//   <script>alert("\{{.X}}")</script>
	// 讨论:
	//   html/template 包不支持反斜杠后面跟随 action。
	//   这通常是一个错误,有更好的解决方案;例如
	//     <script>alert("{{.X}}")</script>
	//   应该可以工作,如果 {{.X}} 是部分转义序列(如 "xA0"),
	//   则将整个序列标记为安全内容:JSStr(`\xA0`)
	ErrPartialEscape

	// ErrRangeLoopReentry:"在 range 循环重新进入时:……"
	// 示例:
	//   <script>var x = [{{range .}}'{{.}},{{end}}]</script>
	// 讨论:
	//   如果对 range 的一次迭代会使其在与先前不同的上下文中结束,
	//   则不存在单一的上下文。在此示例中,缺少一个引号,因此不清楚
	//   {{.}} 是应在 JS 字符串内部还是在 JS 值上下文中。
	//   第二次迭代将产生类似于
	//
	//     <script>var x = ['firstValue,'secondValue]</script>
	ErrRangeLoopReentry

	// ErrSlashAmbig:'/' 可能开始除法或正则表达式。
	// 示例:
	//   <script>
	//     {{if .C}}var x = 1{{end}}
	//     /-{{.N}}/i.test(x) ? doThis : doThat();
	//   </script>
	// 讨论:
	//   上面的示例可能产生 `var x = 1/-2/i.test(s)...`,
	//   其中第一个 '/' 是数学除法运算符;也可能产生 `/-2/i.test(s)`,
	//   其中第一个 '/' 开始一个正则表达式字面量。
	//   检查分支内是否缺少分号,并可能添加括号以明确你的意图。
	ErrSlashAmbig

	// ErrPredefinedEscaper:"预定义转义器……在模板中不被允许"
	// 示例:
	//   <div class={{. | html}}>Hello<div>
	// 讨论:
	//   html/template 包已经对所有管道进行上下文转义,以产生防止代码注入的安全
	//   HTML 输出。使用预定义转义器 "html" 或 "urlquery" 手动转义管道输出是
	//   不必要的,并且可能影响 Go 1.8 及更早版本中转义管道输出的正确性或安全性。
	//
	//   在大多数情况下(如上面的示例),可以通过简单地从管道中移除预定义转义器
	//   并让上下文自动转义器处理管道的转义来解决此错误。在其他情况下,如果
	//   预定义转义器出现在管道中间,后续命令期望转义后的输入,例如
	//     {{.X | html | makeALink}}
	//   其中 makeALink 执行
	//     return `<a href="`+input+`">link</a>`
	//   请考虑重构周围的模板以利用上下文自动转义器,即
	//     <a href="{{.X}}">link</a>
	//
	//   为了简化向 Go 1.9 及更高版本的迁移,"html" 和 "urlquery" 将继续被允许
	//   作为管道中的最后一个命令。但是,如果管道出现在未加引号的属性值上下文中,
	//   则 "html" 不被允许。在新模板中应完全避免使用 "html" 和 "urlquery"。
	ErrPredefinedEscaper

	// ErrJSTemplate:"……出现在 JS 模板字面量中"
	// 示例:
	//     <script>var tmpl = `{{.Interp}}`</script>
	// 讨论:
	//   html/template 包不支持在 JS 模板字面量内部使用 action。
	//
	// Deprecated: 当 action 出现在 JS 模板字面量中时,不再返回 ErrJSTemplate。
	// JS 模板字面量内部的 action 现在会按预期进行转义。
	ErrJSTemplate
)

我们为转义模板时出现的每种错误定义了错误码,但转义后的模板在运行时也可能失败。

输出:"ZgotmplZ" 示例:

<img src="{{.X}}">
其中 {{.X}} 求值为 `javascript:...`

讨论:

"ZgotmplZ" 是一个特殊值,表示不安全的内容在运行时到达了 CSS 或 URL 上下文。
上述示例的输出将是
  <img src="#ZgotmplZ">
如果数据来自可信来源,使用内容类型使其免于过滤:URL(`javascript:...`)。

type FuncMap

type FuncMap = template.FuncMap

type HTML

type HTML string

HTML 封装了已知安全的 HTML 文档片段。 它不应用于来自第三方的 HTML,或包含未闭合标签或注释的 HTML。 可靠的 HTML 净化器的输出和经本包转义的模板输出可以安全地用于 HTML。

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。

type HTMLAttr

type HTMLAttr string

HTMLAttr 封装了来自可信来源的 HTML 属性, 例如 ` dir="ltr"`。

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。

type JS

type JS string

JS 封装了已知安全的 EcmaScript5 表达式,例如 `(x + y * z())`。 模板作者有责任确保类型化表达式不会破坏预期的优先级, 并且不存在语句/表达式歧义,例如传递像 "{ foo: bar() }\n['foo']()" 这样的表达式, 它既是有效的 Expression 也是有效的 Program,但含义完全不同。

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。

使用 JS 包含有效但不可信的 JSON 是不安全的。 一个安全的替代方案是使用 json.Unmarshal 解析 JSON,然后将结果对象 传递到模板中,在 JavaScript 上下文中呈现时它将被转换为净化后的 JSON。

type JSStr

type JSStr string

JSStr 封装了一系列字符,旨在嵌入 JavaScript 表达式中引号之间。 该字符串必须匹配一系列 StringCharacter:

StringCharacter :: SourceCharacter 但不包括 `\` 或 LineTerminator
                 | EscapeSequence

注意不允许 LineContinuation。 JSStr("foo\\nbar") 是可以的,但 JSStr("foo\\\nbar") 不行。

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。

type Srcset

type Srcset string

Srcset 封装了已知安全的 srcset 属性 (参见 https://w3c.github.io/html/semantics-embedded-content.html#element-attrdef-img-srcset)。

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。

type Template

type Template struct {

	// 底层模板的解析树,已更新为 HTML 安全。
	Tree *parse.Tree
	// contains filtered or unexported fields
}

Template 是 "text/template" 的特化 Template,用于产生安全的 HTML 文档片段。

Example (Block)
package main

import (
	"html/template"
	"log"
	"os"
	"strings"
)

func main() {
	const (
		master  = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
		overlay = `{{define "list"}} {{join . ", "}}{{end}} `
	)
	var (
		funcs     = template.FuncMap{"join": strings.Join}
		guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
	)
	masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
	if err != nil {
		log.Fatal(err)
	}
	overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
	if err != nil {
		log.Fatal(err)
	}
	if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
		log.Fatal(err)
	}
	if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
		log.Fatal(err)
	}
}

Output:

Names:
- Gamora
- Groot
- Nebula
- Rocket
- Star-Lord
Names: Gamora, Groot, Nebula, Rocket, Star-Lord
Example (Glob)

Here we demonstrate loading a set of templates from a directory.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create a temporary directory and populate it with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir := createTestDir([]templateFile{
		// T0.tmpl is a plain template file that just invokes T1.
		{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
		// T1.tmpl defines a template, T1 that invokes T2.
		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
		// T2.tmpl defines a template T2.
		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
	})
	// Clean up after the test; another quirk of running as an example.
	defer os.RemoveAll(dir)

	// pattern is the glob pattern used to find all the template files.
	pattern := filepath.Join(dir, "*.tmpl")

	// Here starts the example proper.
	// T0.tmpl is the first name matched, so it becomes the starting template,
	// the value returned by ParseGlob.
	tmpl := template.Must(template.ParseGlob(pattern))

	err := tmpl.Execute(os.Stdout, nil)
	if err != nil {
		log.Fatalf("template execution: %s", err)
	}
}

Output:

T0 invokes T1: (T1 invokes T2: (This is T2))
Example (Helpers)

This example demonstrates one way to share some templates and use them in different contexts. In this variant we add multiple driver templates by hand to an existing bundle of templates.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create a temporary directory and populate it with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir := createTestDir([]templateFile{
		// T1.tmpl defines a template, T1 that invokes T2.
		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
		// T2.tmpl defines a template T2.
		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
	})
	// Clean up after the test; another quirk of running as an example.
	defer os.RemoveAll(dir)

	// pattern is the glob pattern used to find all the template files.
	pattern := filepath.Join(dir, "*.tmpl")

	// Here starts the example proper.
	// Load the helpers.
	templates := template.Must(template.ParseGlob(pattern))
	// Add one driver template to the bunch; we do this with an explicit template definition.
	_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
	if err != nil {
		log.Fatal("parsing driver1: ", err)
	}
	// Add another driver template.
	_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
	if err != nil {
		log.Fatal("parsing driver2: ", err)
	}
	// We load all the templates before execution. This package does not require
	// that behavior but html/template's escaping does, so it's a good habit.
	err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
	if err != nil {
		log.Fatalf("driver1 execution: %s", err)
	}
	err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
	if err != nil {
		log.Fatalf("driver2 execution: %s", err)
	}
}

Output:

Driver 1 calls T1: (T1 invokes T2: (This is T2))
Driver 2 calls T2: (This is T2)
Example (Parsefiles)

Here we demonstrate loading a set of templates from files in different directories

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create different temporary directories and populate them with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir1 := createTestDir([]templateFile{
		// T1.tmpl is a plain template file that just invokes T2.
		{"T1.tmpl", `T1 invokes T2: ({{template "T2"}})`},
	})

	dir2 := createTestDir([]templateFile{
		// T2.tmpl defines a template T2.
		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
	})

	// Clean up after the test; another quirk of running as an example.
	defer func(dirs ...string) {
		for _, dir := range dirs {
			os.RemoveAll(dir)
		}
	}(dir1, dir2)

	// Here starts the example proper.
	// Let's just parse only dir1/T0 and dir2/T2
	paths := []string{
		filepath.Join(dir1, "T1.tmpl"),
		filepath.Join(dir2, "T2.tmpl"),
	}
	tmpl := template.Must(template.ParseFiles(paths...))

	err := tmpl.Execute(os.Stdout, nil)
	if err != nil {
		log.Fatalf("template execution: %s", err)
	}
}

Output:

T1 invokes T2: (This is T2)
Example (Share)

This example demonstrates how to use one group of driver templates with distinct sets of helper templates.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create a temporary directory and populate it with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir := createTestDir([]templateFile{
		// T0.tmpl is a plain template file that just invokes T1.
		{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
		// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
	})
	// Clean up after the test; another quirk of running as an example.
	defer os.RemoveAll(dir)

	// pattern is the glob pattern used to find all the template files.
	pattern := filepath.Join(dir, "*.tmpl")

	// Here starts the example proper.
	// Load the drivers.
	drivers := template.Must(template.ParseGlob(pattern))

	// We must define an implementation of the T2 template. First we clone
	// the drivers, then add a definition of T2 to the template name space.

	// 1. Clone the helper set to create a new name space from which to run them.
	first, err := drivers.Clone()
	if err != nil {
		log.Fatal("cloning helpers: ", err)
	}
	// 2. Define T2, version A, and parse it.
	_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
	if err != nil {
		log.Fatal("parsing T2: ", err)
	}

	// Now repeat the whole thing, using a different version of T2.
	// 1. Clone the drivers.
	second, err := drivers.Clone()
	if err != nil {
		log.Fatal("cloning drivers: ", err)
	}
	// 2. Define T2, version B, and parse it.
	_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
	if err != nil {
		log.Fatal("parsing T2: ", err)
	}

	// Execute the templates in the reverse order to verify the
	// first is unaffected by the second.
	err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
	if err != nil {
		log.Fatalf("second execution: %s", err)
	}
	err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
	if err != nil {
		log.Fatalf("first: execution: %s", err)
	}

}

Output:

T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))

func Must

func Must(t *Template, err error) *Template

Must 是一个辅助函数,它封装了对返回 (*Template, error) 的函数的调用, 如果错误非 nil 则 panic。它旨在用于变量初始化,例如

var t = template.Must(template.New("name").Parse("html"))

func New

func New(name string) *Template

New 分配一个具有给定名称的新 HTML 模板。

func ParseFS

func ParseFS(fs fs.FS, patterns ...string) (*Template, error)

ParseFS 类似于 ParseFilesParseGlob,但从文件系统 fs 读取而不是 从宿主操作系统的文件系统读取。它接受一个 glob 模式列表。 (注意大多数文件名本身就是只匹配自身的 glob 模式。)

func ParseFiles

func ParseFiles(filenames ...string) (*Template, error)

ParseFiles 创建一个新的 Template 并从命名文件中解析模板定义。 返回的模板的名称将具有第一个文件的(基础)名称和(已解析的)内容。 必须至少有一个文件。如果发生错误,解析停止且返回的 *Template 为 nil。

当解析不同目录中同名的多个文件时,最后提到的那个将是最终结果。 例如,ParseFiles("a/foo", "b/foo") 将 "b/foo" 存储为名为 "foo" 的模板, 而 "a/foo" 不可用。

func ParseGlob

func ParseGlob(pattern string) (*Template, error)

ParseGlob 创建一个新的 Template 并从模式标识的文件中解析模板定义。 文件按照 filepath.Match 的语义进行匹配,且模式必须匹配至少一个文件。 返回的模板将具有模式匹配的第一个文件的(基础)名称和(已解析的)内容。 ParseGlob 等价于使用模式匹配的文件列表调用 ParseFiles

当解析不同目录中同名的多个文件时,最后提到的那个将是最终结果。

func (*Template) AddParseTree

func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)

AddParseTree 使用名称和解析树创建一个新模板并将其与 t 关联。

如果 t 或任何关联模板已被执行,则返回错误。

func (*Template) Clone

func (t *Template) Clone() (*Template, error)

Clone 返回模板的副本,包括所有关联模板。实际表示不会被复制, 但关联模板的名称空间会被复制,因此在副本中对 Template.Parse 的进一步调用 将把模板添加到副本而不是原始模板。Template.Clone 可用于准备通用模板, 并在克隆完成后添加变体来与其他模板的变体定义一起使用。

如果 t 已被执行,则返回错误。

func (*Template) DefinedTemplates

func (t *Template) DefinedTemplates() string

DefinedTemplates 返回一个列出已定义模板的字符串, 前缀为 "; defined templates are: "。如果没有已定义的模板, 则返回空字符串。用于生成错误消息。

func (*Template) Delims

func (t *Template) Delims(left, right string) *Template

Delims 将 action 分隔符设置为指定的字符串,用于后续对 Template.ParseParseFilesParseGlob 的调用。嵌套的模板定义将继承这些设置。 空分隔符代表相应的默认值:{{ 或 }}。 返回值是模板,因此调用可以链式进行。

Example
package main

import (
	"html/template"
	"log"
	"os"
)

func main() {
	const text = "<<.Greeting>> {{.Name}}"

	data := struct {
		Greeting string
		Name     string
	}{
		Greeting: "Hello",
		Name:     "Joe",
	}

	t := template.Must(template.New("tpl").Delims("<<", ">>").Parse(text))

	err := t.Execute(os.Stdout, data)
	if err != nil {
		log.Fatal(err)
	}

}

Output:

Hello {{.Name}}

func (*Template) Execute

func (t *Template) Execute(wr io.Writer, data any) error

Execute 将已解析的模板应用到指定的数据对象,将输出写入 wr。 如果执行模板或写入输出时发生错误,执行会停止, 但部分结果可能已经被写入到输出 writer 中。 模板可以安全地并行执行,但如果并行执行共享同一个 Writer, 输出可能会交错。

func (*Template) ExecuteTemplate

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

ExecuteTemplate 将与 t 关联的具有给定名称的模板应用到指定的数据对象, 并将输出写入 wr。如果执行模板或写入输出时发生错误,执行会停止, 但部分结果可能已经被写入到输出 writer 中。 模板可以安全地并行执行,但如果并行执行共享同一个 Writer, 输出可能会交错。

func (*Template) Funcs

func (t *Template) Funcs(funcMap FuncMap) *Template

Funcs 将参数 map 中的元素添加到模板的函数 map 中。 必须在模板解析之前调用。 如果 map 中的值不是具有适当返回类型的函数,则会 panic。 但是,覆盖 map 中的元素是合法的。返回值是模板,因此调用可以链式进行。

func (*Template) Lookup

func (t *Template) Lookup(name string) *Template

Lookup 返回与 t 关联的具有给定名称的模板,如果不存在这样的模板则返回 nil。

func (*Template) Name

func (t *Template) Name() string

Name 返回模板的名称。

func (*Template) New

func (t *Template) New(name string) *Template

New 分配一个与给定模板关联且具有相同分隔符的新 HTML 模板。 关联是传递性的,允许一个模板通过 {{template}} action 调用另一个。

如果具有给定名称的模板已存在,新的 HTML 模板将替换它。 现有模板将被重置并与 t 解除关联。

func (*Template) Option

func (t *Template) Option(opt ...string) *Template

Option 为模板设置选项。选项由字符串描述, 可以是简单字符串或 "key=value"。选项字符串中最多可以有一个等号。 如果选项字符串无法识别或其他方面无效,Option 会 panic。

已知选项:

missingkey:控制执行期间当 map 使用不存在的键进行索引时的行为。

"missingkey=default" 或 "missingkey=invalid"
	默认行为:不做任何操作并继续执行。
	如果打印,索引操作的结果是字符串 "<no value>"。
"missingkey=zero"
	操作返回 map 类型元素的零值。
"missingkey=error"
	执行立即停止并返回错误。

func (*Template) Parse

func (t *Template) Parse(text string) (*Template, error)

Parse 将 text 解析为 t 的模板主体。 text 中的命名模板定义({{define ...}} 或 {{block ...}} 语句) 定义与 t 关联的附加模板,并从 t 本身的定义中移除。

在对 t 或任何关联模板首次使用 Template.Execute 之前, 可以在对 Parse 的连续调用中重新定义模板。 主体仅包含空白和注释的模板定义被视为空的,不会替换现有模板的主体。 这允许使用 Parse 添加新的命名模板定义而不覆盖主模板主体。

func (*Template) ParseFS

func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error)

ParseFS 类似于 Template.ParseFilesTemplate.ParseGlob,但从文件系统 fs 读取 而不是从宿主操作系统的文件系统读取。它接受一个 glob 模式列表。 (注意大多数文件名本身就是只匹配自身的 glob 模式。)

func (*Template) ParseFiles

func (t *Template) ParseFiles(filenames ...string) (*Template, error)

ParseFiles 解析命名文件并将结果模板与 t 关联。如果发生错误, 解析停止且返回的模板为 nil;否则为 t。必须至少有一个文件。

当解析不同目录中同名的多个文件时,最后提到的那个将是最终结果。

如果 t 或任何关联模板已被执行,ParseFiles 返回错误。

func (*Template) ParseGlob

func (t *Template) ParseGlob(pattern string) (*Template, error)

ParseGlob 解析模式标识的文件中的模板定义并将结果模板与 t 关联。 文件按照 filepath.Match 的语义进行匹配,且模式必须匹配至少一个文件。 ParseGlob 等价于使用模式匹配的文件列表调用 t.ParseFiles。

当解析不同目录中同名的多个文件时,最后提到的那个将是最终结果。

如果 t 或任何关联模板已被执行,ParseGlob 返回错误。

func (*Template) Templates

func (t *Template) Templates() []*Template

Templates 返回与 t 关联的模板切片,包括 t 本身。

type URL

type URL string

URL 封装了已知安全的 URL 或 URL 子串(参见 RFC 3986)。 来自可信来源的 URL(如 `javascript:checkThatFormNotEditedBeforeLeavingPage()`) 应该被包含在页面中,但默认情况下动态 `javascript:` URL 会被过滤掉, 因为它们是经常被利用的注入向量。

使用此类型存在安全风险: 封装的内容应来自可信来源,因为它将被原样包含在模板输出中。