Go Template 教程:从入门到精通
Go 语言提供了一套强大且灵活的模板引擎,用于生成动态文本输出。无论您是需要生成 HTML 页面、发送个性化邮件、创建配置文件,还是构建复杂的报告,Go Template 都能助您一臂之力。本教程将带您从 Go Template 的基础知识开始,逐步深入到高级用法和最佳实践。
1. 引言
什么是 Go Template?
Go Template 是 Go 标准库中的两个包:text/template 和 html/template。它们允许您将静态文本与动态数据结合起来,生成定制化的输出。其核心思想是将模板与数据分离,使得内容和逻辑更加清晰。
text/template vs html/template
Go 提供了两个功能几乎相同的模板包,但在用途上有着关键的区别:
text/template: 适用于生成任何纯文本输出,例如电子邮件、配置文件或命令行报告。它不会对内容进行特殊处理。html/template: 专为生成 HTML 输出而设计。它最重要的特性是自动进行上下文感知的转义 (contextual escaping),以防御常见的 Web 安全漏洞,如跨站脚本 (XSS) 攻击。当您渲染可能包含用户提供数据的 HTML 时,始终应该使用html/template。
这两个包共享相同的接口和模板语法,html/template 只是在 text/template 的基础上增加了安全层。
基本结构:静态文本与动作 (Actions)
Go 模板由静态文本和“动作”混合组成。动作被 {{ 和 }} 包裹,用于注入动态数据、控制模板的流程或调用函数。
例如:Hello, {{.Name}}!
这里 Hello, 和 ! 是静态文本,而 {{.Name}} 是一个动作,它会根据传入的数据动态替换为 Name 字段的值。
2. 入门:基本模板用法
我们将从最简单的例子开始,逐步了解如何创建、解析和执行模板。
创建、解析和执行模板 (字符串)
模板的使用通常分为三个步骤:定义模板、解析模板和执行模板。
“`go
package main
import (
“log”
“os”
“text/template” // 使用 text/template 示例
)
func main() {
// 1. 定义一个模板字符串
const tplString = “Hello, {{.Name}}! You are {{.Age}} years old.”
// 2. 创建一个新的模板并解析字符串。
// template.New("greeting") 为模板指定一个名称,这在调试和嵌套模板时很有用。
tmpl, err := template.New("greeting").Parse(tplString)
if err != nil {
log.Fatalf("Error parsing template: %v", err)
}
// 3. 定义要传递给模板的数据
data := struct {
Name string
Age int
}{
Name: "Alice",
Age: 30,
}
// 4. 执行模板,将输出写入 os.Stdout。
// 数据对象作为第二个参数传递。
err = tmpl.Execute(os.Stdout, data)
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
}
“`
输出:
Hello, Alice! You are 30 years old.
在模板中,{{.Name}} 和 {{.Age}} 是动作。.(点) 符号引用传递给模板的当前数据对象。当数据是结构体时,. 允许访问其导出的字段(例如,.Name 访问 Name 字段)。
从文件解析模板
在实际应用中,模板通常存储在单独的文件中。
首先,创建一个名为 templates/hello.tmpl 的文件:
“`html
Hello, {{.Name}}!
Welcome to our website. You are {{.Age}} years old.
“`
然后,在 Go 代码中解析并执行它:
“`go
package main
import (
“html/template” // 为 HTML 输出使用 html/template
“log”
“os”
)
func main() {
// 1. 解析模板文件
// template.Must 是一个辅助函数,如果 ParseFiles 返回错误会 panic。
// 这在启动时初始化的模板中很常见。
tmpl := template.Must(template.ParseFiles(“templates/hello.tmpl”))
// 2. 定义数据
data := struct {
Name string
Age int
}{
Name: "Bob",
Age: 25,
}
// 3. 执行模板,写入 os.Stdout
err := tmpl.Execute(os.Stdout, data)
if err != nil {
log.Fatalf("Error executing template: %v", err)
}
}
“`
输出:
“`html
Hello, Bob!
Welcome to our website. You are 25 years old.
“`
3. 控制结构
Go 模板提供了用于条件逻辑和迭代的动作。
条件渲染 (if/else)
if 动作允许您有条件地渲染内容。如果一个值是其类型的零值 (例如 false, 0, 空字符串, nil 指针, 零长度数组/切片/映射),则被认为是“假”值。
html
Hello, {{.Name}}!
{{if .IsAdmin}}
<p>You are an administrator.</p>
{{else}}
<p>You are a regular user.</p>
{{end}}
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
)
func main() {
const tplString = Hello, {{.Name}}!
{{if .IsAdmin}}
<p>You are an administrator.</p>
{{else}}
<p>You are a regular user.</p>
{{end}}
tmpl := template.Must(template.New(“conditional”).Parse(tplString))
data1 := struct {
Name string
IsAdmin bool
}{
Name: "Charlie",
IsAdmin: true,
}
log.Println("--- Admin User ---")
err := tmpl.Execute(os.Stdout, data1)
if err != nil {
log.Fatalf("Error executing template for admin: %v", err)
}
data2 := struct {
Name string
IsAdmin bool
}{
Name: "David",
IsAdmin: false,
}
log.Println("\n--- Regular User ---")
err = tmpl.Execute(os.Stdout, data2)
if err != nil {
log.Fatalf("Error executing template for regular user: %v", err)
}
}
“`
输出:
“`
— Admin User —
Hello, Charlie!
You are an administrator.
— Regular User —
Hello, David!
You are a regular user.
“`
迭代 (range)
range 动作用于遍历数组、切片、映射或通道。在 range 块内部,. 被设置为当前迭代项。
“`html
Users:
-
{{range .Users}}
- {{.Name}} ({{.Email}})
- No users found.
{{else}}
{{end}}
“`
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
)
type User struct {
Name string
Email string
}
func main() {
const tplString = `
Users:
-
{{range .Users}}
- {{.Name}} ({{.Email}})
- No users found.
{{else}}
{{end}}
`
tmpl := template.Must(template.New(“range_example”).Parse(tplString))
data1 := struct {
Users []User
}{
Users: []User{
{"Eve", "[email protected]"},
{"Frank", "[email protected]"},
},
}
log.Println("--- Users List ---")
err := tmpl.Execute(os.Stdout, data1)
if err != nil {
log.Fatalf("Error executing template with users: %v", err)
}
data2 := struct {
Users []User
}{
Users: []User{}, // 空切片
}
log.Println("\n--- No Users ---")
err = tmpl.Execute(os.Stdout, data2)
if err != nil {
log.Fatalf("Error executing template with no users: %v", err)
}
}
“`
输出:
“`
— Users List —
Users:
- Eve ([email protected])
- Frank ([email protected])
— No Users —
Users:
- No users found.
“`
上下文重绑定 (with)
with 动作将其内部的 . 重新绑定到其管道的值。如果管道的值为空,则跳过该块。
html
{{with .User}}
<h2>User Details:</h2>
<p>Name: {{.Name}}</p>
<p>Email: {{.Email}}</p>
{{else}}
<p>No user details available.</p>
{{end}}
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
)
type UserDetails struct {
Name string
Email string
}
func main() {
const tplString = {{with .User}}
<h2>User Details:</h2>
<p>Name: {{.Name}}</p>
<p>Email: {{.Email}}</p>
{{else}}
<p>No user details available.</p>
{{end}}
tmpl := template.Must(template.New(“with_example”).Parse(tplString))
data1 := struct {
User *UserDetails
}{
User: &UserDetails{"Grace", "[email protected]"},
}
log.Println("--- With User ---")
err := tmpl.Execute(os.Stdout, data1)
if err != nil {
log.Fatalf("Error executing template with user: %v", err)
}
data2 := struct {
User *UserDetails
}{
User: nil, // 没有用户
}
log.Println("\n--- Without User ---")
err = tmpl.Execute(os.Stdout, data2)
if err != nil {
log.Fatalf("Error executing template without user: %v", err)
}
}
“`
输出:
“`
— With User —
<h2>User Details:</h2>
<p>Name: Grace</p>
<p>Email: [email protected]</p>
— Without User —
<p>No user details available.</p>
“`
4. 函数
Go 模板支持一组内置函数,并允许您定义自定义函数。
内建函数
函数使用 functionName arg1 arg2 或 arg1 | functionName arg2(管道)语法调用。
常见的内置函数包括:
- 比较:
eq(等于),ne(不等于),lt(小于),le(小于等于),gt(大于),ge(大于等于)。 - 逻辑:
and,or,not。 - 数据操作:
len(字符串、数组、切片、映射的长度),index(访问数组/切片/映射的元素)。 - 输出:
print,printf,println。 - 转义 (仅限 html/template):
html,js,urlquery。
管道 (Pipelines |)
管道允许链式调用函数,一个函数的输出成为下一个函数的输入。
“`html
Name: {{.Name | printf “%s (Length: %d)” .Name | upper}}
Is Adult: {{if ge .Age 18}}Yes{{else}}No{{end}}
“`
注意:upper 不是内置函数。这个例子假设注册了一个自定义的 upper 函数。
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
“strings” // 用于 strings.ToUpper
)
func main() {
const tplString = `
Name: {{.Name | printf “%s (Length: %d)” .Name | upper}}
Is Adult: {{if ge .Age 18}}Yes{{else}}No{{end}}
`
// 为自定义函数创建一个 FuncMap
funcMap := template.FuncMap{
“upper”: strings.ToUpper, // 注册 strings.ToUpper
}
// 在解析模板之前注册 FuncMap
tmpl := template.Must(template.New(“functions_example”).Funcs(funcMap).Parse(tplString))
data := struct {
Name string
Age int
}{
Name: “Heidi”,
Age: 22,
}
err := tmpl.Execute(os.Stdout, data)
if err != nil {
log.Fatalf(“Error executing template: %v”, err)
}
}
“`
**输出:**
“`
Name: HEIDI (Length: 5)
Is Adult: Yes
“`
#### 自定义函数 (`FuncMap`)
您可以将自己的 Go 函数注册到模板中。这些函数必须返回一个值,或者两个值,其中第二个值是 `error` 类型。
“`go
package main
import (
“html/template”
“log”
“os”
“strings”
)
// customGreeting 是一个自定义函数,接受一个名称并返回问候语。
func customGreeting(name string) string {
return “Greetings, ” + name + “!”
}
// add 接受两个整数并返回它们的和。
func add(a, b int) int {
return a + b
}
func main() {
const tplString = `
{{.Name | customGreeting}}
The sum of 5 and 7 is: {{add 5 7}}
`
// 创建一个 FuncMap 来注册自定义函数
funcMap := template.FuncMap{
“customGreeting”: customGreeting,
“add”: add,
“upper”: strings.ToUpper, // 再次使用 strings.ToUpper
}
// 在解析模板之前注册 FuncMap
tmpl := template.Must(template.New(“custom_funcs”).Funcs(funcMap).Parse(tplString))
data := struct {
Name string
}{
Name: “Irene”,
}
err := tmpl.Execute(os.Stdout, data)
if err != nil {
log.Fatalf(“Error executing template: %v”, err)
}
}
“`
**输出:**
“`
Greetings, Irene!
The sum of 5 and 7 is: 12
“`
### 5. 高级特性
#### 嵌套模板与布局 (`define`, `template`, `block`)
对于大型应用程序,您会希望重用 HTML 的公共部分(如页眉、页脚、侧边栏)。Go 模板支持嵌套模板和布局。
* **`define`**: 在模板文件中定义一个具名模板。
* **`template`**: 包含另一个具名模板。您可以选择性地向包含的模板传递数据。
* **`block`**: 是 `define` 后立即跟 `template` 的简写。它定义一个具名模板,然后执行它。这对于定义可以被其他模板覆盖的默认内容非常有用。
创建一个 `templates/layout.tmpl` 文件:
“`html
My Website
{{template “navbar” .}}
{{block “content” .}}
Default content goes here.
{{end}}
{{define “navbar”}}
{{end}}
“`
创建一个 `templates/home.tmpl` 文件:
“`html
{{template “layout.tmpl” .}}
{{define “title”}}Home Page{{end}}
{{define “content”}}
Welcome!
This is the home page content.
Current user: {{.UserName}}
{{end}}
“`
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
)
func main() {
// 解析 “templates” 目录中的所有模板。
// ParseGlob 对于加载多个模板非常有用。
tmpl := template.Must(template.ParseGlob(“templates/*.tmpl”))
data := struct {
UserName string
}{
UserName: “John Doe”,
}
// 执行 “home.tmpl” 模板,它反过来会使用 “layout.tmpl”
err := tmpl.ExecuteTemplate(os.Stdout, “home.tmpl”, data)
if err != nil {
log.Fatalf(“Error executing template: %v”, err)
}
}
“`
**输出:**
“`html
My Website
Welcome!
This is the home page content.
Current user: John Doe
“`
#### 模板变量 (`$`)
在 `range` 和 `with` 块内部,`.(点)` 会改变上下文。要访问传递给模板的原始数据,或来自外部作用域的值,您可以使用 `$` 定义一个变量。
“`html
{{$top := .}} {{/* 将顶层数据存储在 $top 中 */}}
Users:
-
{{range .Users}}
- {{.Name}} ({{$top.SiteName}})
{{/* 访问顶层 SiteName */}}
{{end}}
“`
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
)
type UserWithSite struct {
Name string
}
func main() {
const tplString = `
{{$top := .}}
Users:
-
{{range .Users}}
- {{.Name}} ({{$top.SiteName}})
{{end}}
`
tmpl := template.Must(template.New(“variables_example”).Parse(tplString))
data := struct {
SiteName string
Users []UserWithSite
}{
SiteName: “Awesome Site”,
Users: []UserWithSite{
{“Jack”},
{“Kelly”},
},
}
err := tmpl.Execute(os.Stdout, data)
if err != nil {
log.Fatalf(“Error executing template: %v”, err)
}
}
“`
**输出:**
“`
Users:
- Jack (Awesome Site)
- Kelly (Awesome Site)
“`
#### 空白字符控制 (`{{-`, `-}}`)
模板会输出动作之间的所有内容,包括空白字符。您可以使用 `{{-` (去除前导空白) 和 `-}}` (去除尾随空白) 来控制动作周围的空白字符。
“`html
Start:
{{- ” Hello ” -}}
{{- “World ” -}}
:End
“`
Go 代码示例:
“`go
package main
import (
“html/template”
“log”
“os”
)
func main() {
const tplString = `
Start:
{{- ” Hello ” -}}
{{- “World ” -}}
:End
`
tmpl := template.Must(template.New(“whitespace_example”).Parse(tplString))
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
log.Fatalf(“Error executing template: %v”, err)
}
}
“`
**输出:**
“`
Start:
HelloWorld:End
“`
#### `html/template` 的上下文感知转义 (XSS 防护)
`html/template` 会根据内容所处的上下文(HTML、JavaScript、URL、CSS)自动转义数据,以防止 XSS 攻击。
“`html
User Input: {{.UserInput}}
Click Me
“`
如果 `UserInput` 包含 ``,`html/template` 会将其转义为 `<script>alert('xss')</script>`。如果 `UserURL` 包含 `javascript:alert(‘xss’)`,它将被净化。
如果您有一些您确定是安全 HTML 的内容,并希望直接渲染而不转义,可以将其封装在 `template.HTML` 类型中。**请务必谨慎使用此功能**,因为它会绕过安全机制。
“`go
package main
import (
“html/template”
“log”
“os”
)
func main() {
const tplString = `
Escaped User Input: {{.UserInput}}
Safe HTML (Unescaped): {{.SafeHTML}}
Click Me
`
tmpl := template.Must(template.New(“escaping_example”).Parse(tplString))
data := struct {
UserInput string
SafeHTML template.HTML // 使用 template.HTML 标记已知安全的内容
UserURL string
UserName string
}{
UserInput: ““,
SafeHTML: “This is safe HTML.“,
UserURL: “javascript:alert(‘XSS’)”,
UserName: `Robert’); alert(‘XSS’); var x = (‘`,
}
err := tmpl.Execute(os.Stdout, data)
if err != nil {
log.Fatalf(“Error executing template: %v”, err)
}
}
“`
**输出:**
“`html
Escaped User Input: <script>alert('XSS')</script>
Safe HTML (Unescaped): This is safe HTML.
Click Me
“`
请注意 `html/template` 如何自动将 `javascript:` URL 净化为 `#ZgotmplZ` 并转义 JavaScript 字符串。
### 6. 最佳实践与技巧
* **始终为 HTML 输出使用 `html/template`。** 这是最重要的安全建议。
* **错误处理:** 对于在应用程序启动时加载的模板,使用 `template.Must(template.ParseFiles(…))`。如果出现错误,它会 panic,表明是配置问题。对于运行时解析的模板(例如,来自用户输入),请明确处理错误。
* **加载多个模板:** 使用 `template.ParseFiles` 加载特定文件,或使用 `template.ParseGlob` 加载符合模式的文件(例如 `*.tmpl`),以便加载多个模板。
* **组织模板:** 将模板放在专门的目录中(例如 `templates/`),并使用嵌套模板来重用公共组件,如页眉、页脚和导航。
* **传递特定数据:** 不要传递一个大型、通用的数据结构,而是只将与模板相关的数据传递过去,以保持模板简洁和专注。
* **模板中避免复杂逻辑:** 模板用于展示。将复杂的业务逻辑保留在您的 Go 代码中,并将处理后的数据传递给模板。
* **利用外部库:** 对于更高级的模板函数(例如字符串操作、日期格式化),可以考虑使用 [Sprig](https://github.com/Masterminds/sprig) 等库,它提供了 100 多个额外的函数。
—
希望这份教程能帮助您全面理解并熟练运用 Go Template!