Go Template 教程:从入门到精通 – wiki大全


Go Template 教程:从入门到精通

Go 语言提供了一套强大且灵活的模板引擎,用于生成动态文本输出。无论您是需要生成 HTML 页面、发送个性化邮件、创建配置文件,还是构建复杂的报告,Go Template 都能助您一臂之力。本教程将带您从 Go Template 的基础知识开始,逐步深入到高级用法和最佳实践。

1. 引言

什么是 Go Template?

Go Template 是 Go 标准库中的两个包:text/templatehtml/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




Greeting

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




Greeting

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}})
  • {{else}}

  • No users found.
  • {{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}})
  • {{else}}

  • No users found.
  • {{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:

— 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 arg2arg1 | 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



{{block “title” .}}Default Title{{end}}

My Website

{{template “navbar” .}}


{{block “content” .}}

Default content goes here.

{{end}}

© 2025 My Company


{{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



Home Page

My Website

Welcome!

This is the home page content.

Current user: John Doe

© 2025 My Company



“`

#### 模板变量 (`$`)

在 `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!

滚动至顶部