Go语言template模板语法完全指南

Golang text/template & html/template使用教程

文章目录

在 Go 语言中,模板机制(text/templatehtml/template 包)提供了一种强大的、数据驱动的方式来生成文本或 HTML 输出。无论你是生成配置文件、邮件内容,还是构建 Web 应用程序的动态页面,理解 Go 模板都是必不可少的。

如果你正在生成 HTML 输出,强烈建议使用 html/template 包,因为它提供了与 text/template 相同的接口,但增加了自动上下文转义(auto-escaping)功能,以确保输出安全,抵御代码注入攻击(如 XSS)。

本文将详细介绍模板的核心语法、控制结构、数据访问和高级功能。

一、 模板基础与核心概念

1. 动作(Actions)与定界符(Delimiters)

模板文本是 UTF-8 编码的普通文本,所有非动作部分都将按原样复制到输出中。

模板中的“动作”(Actions)——即数据评估或控制结构——默认被 {{}} 包裹。

  • 修改定界符: 你可以使用 Template.Delims(left, right) 方法来修改定界符。

示例 1.1:基本结构与数据输出(使用 html/template

package main

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

type Inventory struct {
    Material string
    Count uint
}

func main() {
    // 模板定义
    const tpl = "{{.Count}} items are made of {{.Material}}"

    // 准备数据
    sweaters := Inventory{"wool", 17}

    // 1. 创建并解析模板
    tmpl, err := template.New("test").Parse(tpl)
    if err != nil {
        log.Fatal(err)
    }

    // 2. 执行模板
    err = tmpl.Execute(os.Stdout, sweaters)
    // 输出: 17 items are made of wool
    if err != nil {
        log.Fatal(err)
    }
}

(注意:虽然此示例使用了 html/template,但其基本语法与 text/template 相同。这个例子展示了 text/template 文档中描述的简单结构。)

2. 空白符修剪(Whitespace Trimming)

为了方便模板源码格式化,Go 模板提供了空白符修剪标记:

  • 左侧修剪: 如果左定界符后紧跟一个减号和空格,例如 {{- pipeline}},则将修剪紧接在前的所有尾随空白符。
  • 右侧修剪: 如果右定界符前是空格和减号,例如 {{pipeline -}},则将修剪紧接在后的所有引导空白符。

示例 1.2:空白符修剪

// 模板源: "{{23 -}} < {{- 45}}"
// 输出: "23<45"
// (在执行 template.Execute 时,前后的空白都被移除)

3. 注释(Comments)

注释以 {{/* 开始,以 */}} 结束,它们在输出中会被丢弃。注释不能嵌套。

{{/* 这是一个注释,不会出现在最终输出中 */}}
{{- /* 带有空白修剪的注释 */ -}}

二、 数据访问和参数(Arguments)

模板的执行是通过将模板应用于一个数据结构来完成的。在模板执行过程中,游标(cursor)由点 . 表示,称为 “dot”,它代表了数据结构中当前位置的值。

1. 基础数据参数

参数(Argument)是一个简单值,可以表示以下内容:

  1. 常量: Go 语法中的布尔、字符串、字符、整数、浮点数或复数常量。
  2. nil 关键字: 代表 Go 中的未类型化 nil。
  3. 点 (.): 表示当前数据上下文(dot)的值。
  4. 变量名: 以美元符号 $ 开头的字母数字字符串(包括 $piOver2 或单独的 $)。
  5. 圆括号: 用于分组,如 print (.F1 arg1) (.F2 arg2)

2. 访问结构体字段、Map 键和方法

通过在 . 或变量名后加点来访问数据结构中的元素。

语法描述示例
.Field访问当前数据(dot)的字段。字段必须是结构体的一部分。{{.User.Name}}
.Key访问当前数据(dot)的 Map 键。Map 键可以是字母数字标识符,不必以大写字母开头。{{.MapData.key1}}
.Method访问当前数据(dot)的无参数方法。该方法必须返回一个值,或两个值(第二个为 error)。{{.CalculateTotal}}
链式调用字段、键和方法可以深度链式调用。{{.Field1.Key1.Method1}}
变量调用字段、键和方法也可以基于变量进行评估。{{$user.Name}}, {{$user.Method}}

注意: 如果参数是指针,模板引擎会自动解引用到其基础类型。

3. 变量(Variables)

你可以在动作中使用变量来捕获 Pipeline 的结果。

语法描述
$variable := pipeline声明并初始化一个变量。该动作不产生输出。
$variable = pipeline赋值给一个已声明的变量。

变量的作用域延伸到声明它的控制结构 (if, with, range) 的 end 动作。当模板执行开始时,特殊变量 $ 被设置为传递给 Execute 的数据参数(即 dot 的起始值)。


三、 控制结构(Control Flow)

1. 条件语句:if, else, else if

if 动作用于检查 Pipeline 的值是否为空 (empty)。

空值定义: false0、任何 nil 指针或接口值,以及任何长度为零的数组、切片、Map 或字符串。

语法描述
{{if pipeline}} T1 {{end}}如果 Pipeline 不为空,执行 T1。
{{if pipeline}} T1 {{else}} T0 {{end}}如果 Pipeline 不为空,执行 T1;否则执行 T0。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}简化 if-else 链。

示例 3.1:If 语句

// 假设传入数据 b 是一个 struct
// {{ if (gt .Stars 4.0) }}"{{.Name }}" is a great book.{{ else }}"{{.Name}}" is not a great book.{{ end }}
// 如果 .Stars > 4.0 (使用了内置函数 gt),则执行 T1。

2. 迭代语句:range, else, break, continue

range 动作用于遍历数组、切片、Map、整数或通道。

语法描述
{{range pipeline}} T1 {{end}}遍历 Pipeline 的元素。dot 被设置为连续的元素值。如果长度为零,无输出。
{{range $index, $element := pipeline}} T1 {{end}}声明两个变量:$index$element,分别设置为数组/切片索引或 Map 键,以及元素值。
{{range pipeline}} T1 {{else}} T0 {{end}}如果 Pipeline 长度为零,执行 T0。
{{break}}提前结束最内层的 range 循环。
{{continue}}停止当前迭代,开始 range 循环的下一次迭代。

示例 3.2:Range 语句

{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
// 如果 .Items 列表非空,则遍历 Items 并为每个元素生成一个 <div> 标签。
// 如果 .Items 列表为空,则输出 <div><strong>no rows</strong></div>。

3. 上下文切换:with, else

with 动作允许你在局部范围内将 dot 的值设置为 Pipeline 的结果。

语法描述
{{with pipeline}} T1 {{end}}如果 Pipeline 非空,将 dot 设置为 Pipeline 的值,并执行 T1。
{{with pipeline}} T1 {{else}} T0 {{end}}如果 Pipeline 非空,执行 T1;否则执行 T0。

示例 3.3:With 语句

{{with .Gift -}} Thank you for the lovely {{.}}. {{end}}
// 如果 .Gift 不为空,则在 with 块内,. 指代 .Gift 的值。
// 注意这里使用了 {{- 语法来修剪前后的空白符。

四、 Pipeline 和函数(Functions)

1. Pipeline 概念

Pipeline 是一系列命令(command)的序列,它们之间用管道符 | 分隔。

  • 执行方式: 前一个命令的结果会被作为最后一个参数传递给后一个命令。
  • 命令类型: 命令可以是一个简单值(Argument),也可以是函数或方法调用。

示例 4.1:Pipeline

{{"output" | printf "%s" | printf "%q"}}
// 1. "output" 被传递给第一个 printf。
// 2. 第一个 printf 的结果作为最后一个参数传递给第二个 printf。
// 最终输出: "\"output\""

2. 预定义函数(Predefined Functions)

模板引擎默认提供了一系列全局预定义函数。

函数类别函数名描述
逻辑and, or, not布尔逻辑操作。非零值视为 true,零值视为 false
比较eq, ne, lt, le, gt, ge等于 (==), 不等于 (!=), 小于, 小于等于, 大于, 大于等于。eq 可以接受两个或多个参数,检查第二个及后续参数是否等于第一个参数。
数据处理len返回参数的整数长度。
格式化print, printf, println分别是 fmt.Sprint, fmt.Sprintf, fmt.Sprintln 的别名。
索引/切片index通过参数索引其第一个参数,如 index x 1 2 相当于 Go 中的 x
slice对第一个参数进行切片操作,如 slice x 1 2 相当于 Go 中的 x[1:2]
调用call调用第一个参数(必须是函数类型),其余参数作为其参数。

注意 (安全转义):text/template 中,还存在 htmljsurlquery 等转义函数。但在用于 HTML 输出的 html/template 包中,这些函数通常是不可用或被禁止的,因为 html/template 会进行自动上下文转义。

3. 自定义函数

你可以通过 Template.Funcs 方法向模板添加自定义函数,使用 FuncMap

  • 时间要求: Funcs 必须在模板被解析之前调用。
  • 返回值: 自定义函数必须返回一个值,或两个值(第二个值必须是 error 类型)。

示例 4.2:添加自定义函数

package main

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

func main() {
    // 定义 FuncMap,注册自定义函数 (例如 strings.ToLower, strings.Repeat)
    funcMap := template.FuncMap{
        "lower": strings.ToLower,
        "repeat": func(s string) string { return strings.Repeat(s, 2) },
    }

    const tmpl = `{{ . | lower | repeat }}`

    // 1. 创建模板,通过 Funcs 添加函数,然后解析
    parsedTmpl, err := template.New("t").Funcs(funcMap).Parse(tmpl)
    if err != nil {
        log.Fatal(err)
    }

    // 2. 执行模板
    err = parsedTmpl.Execute(os.Stdout, "ABC")
    // 输出: abcabc
}

(此例展示了如何将 lowerrepeat 函数链接在 Pipeline 中使用。)


五、 模板关联与复用

Go 模板可以关联零个或多个其他模板,形成一个模板命名空间

1. 模板定义与调用

动作描述
{{define "name"}} T1 {{end}}定义一个名为 "name" 的模板 T1。定义必须出现在模板的顶层。
{{template "name"}}执行名为 "name" 的关联模板,数据上下文为 nil
{{template "name" pipeline}}执行名为 "name" 的关联模板,数据上下文设置为 Pipeline 的值。
{{block "name" pipeline}} T1 {{end}}块(Block)是定义模板 {{define "name"}} T1 {{end}} 然后立即执行 {{template "name" pipeline}} 的简写。常用于定义根模板,以便通过重定义块模板来自定义内容。

示例 5.1:模板关联与调用

假设我们定义了 T1、T2 和 T3 模板:

{{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}
// 最终输出: ONE TWO

2. 模板解析与管理方法

Template 类型提供了一系列方法来加载、管理和执行模板。

方法/函数描述
template.New(name string)分配一个新的模板实例。
template.Must(t, err)包装一个返回 (*Template, error) 的函数调用,如果出错则 panic
t.Parse(text string)解析文本作为模板主体。
template.ParseFiles(filenames ...string)创建新模板并从指定文件解析定义。返回的模板名称是第一个文件的基础名。
template.ParseGlob(pattern string)创建新模板并解析匹配通配符模式的文件中的定义。
t.ParseFiles(...) / t.ParseGlob(...)将文件中的模板定义解析并关联到现有模板 t
t.Clone()返回模板及其所有关联模板的副本,创建新的命名空间。
t.Execute(wr, data)对解析后的模板应用指定数据,并写入 wr
t.ExecuteTemplate(wr, name, data)执行与 t 关联的、具有指定名称的模板。
t.Lookup(name string)查找与 t 关联的、具有给定名称的模板。

六、 HTML 模板的安全特性 (html/template)

当你使用 html/template 包时,它会自动应用安全措施。

1. 上下文自动转义(Contextual Autoescaping)

html/template 理解 HTML、CSS、JavaScript 和 URI 等上下文。它会在解析时,根据数据插入的位置,自动添加必要的转义函数,以防止恶意内容破坏 HTML 结构。

html/template 自动转义示例:

// 假设数据 {{.}} 是一个包含攻击脚本的字符串:
// "<script>alert('you have been pwned')</script>"

// 使用 html/template 执行模板 `Hello, {{.}}!`:
// 生产安全的、转义后的 HTML 输出:
// Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!

在解析时,模板引擎可能会将简单的动作 {{.}} 改写为包含内部转义函数的 Pipeline,例如:

<a href="/search?q={{.}}">{{.}}</a>
<!-- 在解析时可能变为 (内部别名): -->
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>

2. 区分命名空间和 data- 属性

html/template 在确定上下文时,会特殊处理带有命名空间的属性(如 <a my:href="{{.}}">,会被视为 href 属性)。同时,带有 data- 前缀的属性也会被特殊处理。

3. 类型化字符串(Typed Strings)

默认情况下,模板引擎假设所有 Pipeline 产生的是纯文本字符串,并对其进行转义。

如果你确定某些数据是安全且受信任的内容(例如,已经通过 HTML 净化器处理过),你可以将其标记为特定的类型,从而豁免自动转义。

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

类型描述危险提示
template.HTML封装已知安全的 HTML 文档片段。tmpl.Execute(out, template.HTML(World))
template.CSS封装已知安全的 CSS 内容(如样式表、规则或声明)。安全风险高
template.JS封装已知安全的 EcmaScript5 表达式。安全风险高
template.URL封装已知安全的 URL 或 URL 子字符串。用于绕过 javascript: 等协议过滤。
template.JSStr封装用于嵌入 JavaScript 表达式引号中的字符序列。安全风险高
template.HTMLAttr封装 HTML 属性(例如 dir="ltr")。安全风险高
template.Srcset封装安全的 srcset 属性。安全风险高

4. 运行时错误和过滤

如果模板执行时,非信任数据在 CSS 或 URL 上下文中评估出不安全的内容(例如,URL 为 javascript:...),html/template 不会输出该内容,而是使用特殊值 #ZgotmplZ 替换它。

// 模板: <img src="{{.X}}">
// 假设 .X 评估为 `javascript:...`
// 输出: <img src="#ZgotmplZ">

总结

Golang 模板引擎提供了一个简洁而强大的语法,通过 .| 实现数据访问和处理,并通过 if/range/with 等动作实现流程控制。在使用 html/template 时,开发者可以受益于自动转义的安全机制,但必须了解上下文转义的工作原理和使用类型化字符串(如 template.HTML)带来的安全责任。理解这些基础和安全特性,你就能编写出高效且安全的 Go 应用程序模板。

类比理解: Go 模板就像一个智能化的厨师(模板引擎)在厨房(Go 程序)中工作。你提供给厨师食谱(模板文件)和食材(数据结构)。厨师会自动将食材安全地整合到食谱中。如果食材是纯文本,厨师会进行必要的防护(如 HTML 转义);如果你明确告诉厨师这块食材(如 template.HTML)是已经消毒且安全的,厨师才会直接使用,但风险自负。


也可以看看