html/template - 安全的 HTML 模板
html/template 包实现了数据驱动的模板引擎,用于生成可对抗代码注入的安全 HTML 输出。
概述
html/template 包提供了与 text/template 相同的接口,但会自动对 HTML 输出进行转义,防止代码注入攻击。它理解 HTML、CSS、JavaScript 和 URI 上下文,并根据上下文自动添加适当的转义函数。
包导入:
import "html/template"
基本使用:
// 1. 创建模板
tmpl, err := template.New("name").Parse("Hello, {{.}}!")
// 2. 执行模板
err = tmpl.Execute(os.Stdout, "World")
// 3. 从文件解析
tmpl, err = template.ParseFiles("index.html")
// 4. 执行命名模板
err = tmpl.ExecuteTemplate(w, "index", data)
典型示例:
示例 1:基本模板:
package main
import (
"html/template"
"os"
)
func main() {
// 创建并解析模板
tmpl, err := template.New("greet").Parse("Hello, {{.}}!")
if err != nil {
panic(err)
}
// 执行模板
tmpl.Execute(os.Stdout, "World")
}
运行:
$ go run main.go
Hello, World!
示例 2:自动转义防止 XSS:
package main
import (
"html/template"
"os"
)
func main() {
// html/template 会自动转义危险内容
tmpl, _ := template.New("test").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
// 恶意输入
malicious := "<script>alert('XSS')</script>"
// 执行模板(自动转义)
tmpl.ExecuteTemplate(os.Stdout, "T", malicious)
}
运行:
$ go run main.go
Hello, <script>alert('XSS')</script>!
示例 3:结构体字段访问:
package main
import (
"fmt"
"html/template"
"os"
)
type Person struct {
Name string
Age int
Addr string
}
func main() {
tmpl := `
<!DOCTYPE html>
<html>
<head><title>{{.Name}}</title></head>
<body>
<p>Name: {{.Name}}</p>
<p>Age: {{.Age}}</p>
<p>Address: {{.Addr}}</p>
</body>
</html>`
t, _ := template.New("person").Parse(tmpl)
person := Person{
Name: "Alice",
Age: 25,
Addr: "<script>alert('XSS')</script>",
}
t.Execute(os.Stdout, person)
}
运行:
$ go run main.go
<!DOCTYPE html>
<html>
<head><title>Alice</title></head>
<body>
<p>Name: Alice</p>
<p>Age: 25</p>
<p>Address: <script>alert('XSS')</script></p>
</body>
</html>
示例 4:从文件加载模板:
// index.html 文件内容:
/*
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<h1>{{.Heading}}</h1>
<p>{{.Content}}</p>
</body>
</html>
*/
package main
import (
"html/template"
"net/http"
)
type PageData struct {
Title string
Heading string
Content string
}
func handler(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFiles("index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := PageData{
Title: "My Page",
Heading: "Welcome",
Content: "<script>alert('XSS')</script>",
}
tmpl.Execute(w, data)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
一、核心类型(按字母顺序)
ErrorCode 类型
ErrorCode
定义:
type ErrorCode string
说明:
- 模板错误代码类型
- 用于标识不同类型的模板错误
常量:
const (
ErrOK ErrorCode = "OK"
ErrPredefinedEscaper ErrorCode = "ErrPredefinedEscaper"
ErrNoSuchTemplate ErrorCode = "ErrNoSuchTemplate"
ErrOutputTimeout ErrorCode = "ErrOutputTimeout"
ErrBadTemplate ErrorCode = "ErrBadTemplate"
ErrBadContext ErrorCode = "ErrBadContext"
ErrPartialResult ErrorCode = "ErrPartialResult"
ErrUnsafe ErrorCode = "ErrUnsafe"
ErrUnknown ErrorCode = "ErrUnknown"
)
Error 类型
Error
定义:
type Error struct {
Err error
Code ErrorCode
}
说明:
- 模板执行错误类型
- 包含具体错误和错误代码
方法:
Error() string- 错误信息
示例:
package main
import (
"fmt"
"html/template"
"os"
)
func main() {
// 无效的模板语法
_, err := template.New("test").Parse("{{.Invalid")
if err != nil {
if templateErr, ok := err.(*template.Error); ok {
fmt.Printf("错误代码:%s\n", templateErr.Code)
fmt.Printf("错误信息:%s\n", templateErr.Err)
}
}
}
FuncMap 类型
FuncMap
定义:
type FuncMap map[string]interface{}
说明:
- 模板函数映射表
- 键为函数名,值为函数
示例:
package main
import (
"fmt"
"html/template"
"os"
"strings"
)
func main() {
// 自定义函数
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"title": strings.Title,
}
tmpl := `{{.Name | upper}}
{{.Name | lower}}
{{.Name | title}}`
t, _ := template.New("test").Funcs(funcMap).Parse(tmpl)
data := struct{ Name string }{"hello world"}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
HELLO WORLD
hello world
Hello World
HTML 类型
HTML
定义:
type HTML string
说明:
- 安全的 HTML 字符串类型
- 不会被自动转义
- 用于标记已知安全的 HTML 内容
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `{{.SafeHTML}}`
t, _ := template.New("test").Parse(tmpl)
data := struct {
SafeHTML template.HTML
}{
SafeHTML: template.HTML("<b>Bold Text</b>"),
}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
<b>Bold Text</b>
HTMLAttr 类型
HTMLAttr
定义:
type HTMLAttr string
说明:
- 安全的 HTML 属性类型
- 用于 HTML 标签的属性名
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `<div {{.Attr}}="value">Content</div>`
t, _ := template.New("test").Parse(tmpl)
data := struct {
Attr template.HTMLAttr
}{
Attr: template.HTMLAttr(`class="highlight"`),
}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
<div class="highlight"="value">Content</div>
JS 类型
JS
定义:
type JS string
说明:
- 安全的 JavaScript 代码类型
- 不会被自动转义
- 用于嵌入已知的安全 JavaScript
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `<script>{{.Code}}</script>`
t, _ := template.New("test").Parse(tmpl)
data := struct {
Code template.JS
}{
Code: template.JS(`alert("Hello");`),
}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
<script>alert("Hello");</script>
JSStr 类型
JSStr
定义:
type JSStr string
说明:
- 安全的 JavaScript 字符串类型
- 用于 JavaScript 上下文中的字符串值
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `<script>var msg = {{.Msg}};</script>`
t, _ := template.New("test").Parse(tmpl)
data := struct {
Msg template.JSStr
}{
Msg: template.JSStr(`"Hello, World!"`),
}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
<script>var msg = "Hello, World!";</script>
Srcset 类型
Srcset
定义:
type Srcset string
说明:
- 安全的 srcset 属性类型
- 用于 img 标签的 srcset 属性
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `<img srcset="{{.Src}}" alt="image">`
t, _ := template.New("test").Parse(tmpl)
data := struct {
Src template.Srcset
}{
Src: template.Srcset("image-320w.jpg 320w, image-480w.jpg 480w"),
}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
<img srcset="image-320w.jpg 320w, image-480w.jpg 480w" alt="image">
Template 类型
Template
定义:
type Template struct {
// 内部字段
}
说明:
- 模板的主要类型
- 表示一个已解析的模板
- 可并发安全执行
方法:
AddParseTree(name, tree) (*Template, error)- 添加解析树Clone() (*Template, error)- 克隆模板DefinedNames() []string- 返回已定义的模板名Delims(left, right)- 设置分隔符Escape() error- 应用转义Execute(wr, data) error- 执行模板ExecuteTemplate(wr, name, data) error- 执行命名模板Funcs(funcMap)- 添加自定义函数Lookup(name) *Template- 查找命名模板Name() string- 返回模板名New(name)- 创建新模板Option(opt)- 设置选项Parse(text)- 解析模板字符串ParseFiles(filenames)- 解析文件ParseGlob(pattern)- 解析匹配的文件
URL 类型
URL
定义:
type URL string
说明:
- 安全的 URL 类型
- 不会被自动转义
- 用于已知的安全 URL
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `<a href="{{.URL}}">Link</a>`
t, _ := template.New("test").Parse(tmpl)
data := struct {
URL template.URL
}{
URL: template.URL("https://example.com"),
}
t.Execute(os.Stdout, data)
}
运行:
$ go run main.go
<a href="https://example.com">Link</a>
二、核心函数(按字母顺序)
Must - 错误处理辅助函数
**Must(t Template, err error) Template
说明:
- 辅助函数,用于简化错误处理
- 如果 err 不为 nil,则 panic
- 否则返回 t
- 常用于包变量初始化
定义:
func Must(t *Template, err error) *Template
示例:
package main
import (
"html/template"
"os"
)
// 使用 Must 简化初始化
var tmpl = template.Must(template.New("test").Parse("Hello, {{.}}!"))
func main() {
tmpl.Execute(os.Stdout, "World")
}
运行:
$ go run main.go
Hello, World!
New - 创建模板
*New(name string) Template
说明:
- 创建指定名称的新模板
- 通常与 Parse 或 ParseFiles 链式调用
定义:
func New(name string) *Template
示例:
package main
import (
"fmt"
"html/template"
"os"
)
func main() {
// 链式调用
tmpl := template.New("greet")
tmpl = template.Must(tmpl.Parse("Hello, {{.}}!"))
// 或一行完成
tmpl2 := template.Must(template.New("farewell").Parse("Goodbye, {{.}}!"))
tmpl.Execute(os.Stdout, "Alice")
fmt.Println()
tmpl2.Execute(os.Stdout, "Alice")
}
运行:
$ go run main.go
Hello, Alice!
Goodbye, Alice!
ParseFiles - 解析多个文件
*ParseFiles(filenames …string) (Template, error)
说明:
- 解析一个或多个模板文件
- 返回的模板名为第一个文件的文件名(不含扩展名)
- 至少需要一个文件
- 如果出错返回 nil
定义:
func ParseFiles(filenames ...string) (*Template, error)
示例:
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 解析多个文件
tmpl, err := template.ParseFiles(
"header.html",
"content.html",
"footer.html",
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.Execute(w, nil)
}
ParseGlob - 解析匹配的文件
*ParseGlob(pattern string) (Template, error)
说明:
- 解析匹配 glob 模式的所有文件
- 返回的模板名为第一个匹配文件的文件名
- 至少需要一个匹配文件
定义:
func ParseGlob(pattern string) (*Template, error)
示例:
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// 解析所有 .html 文件
tmpl, err := template.ParseGlob("templates/*.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmpl.Execute(w, nil)
}
三、Template 方法详解
AddParseTree - 添加解析树
**AddParseTree(name string, tree parse.Tree) (Template, error)
说明:
- 为模板添加解析树
- 用于底层模板操作
Clone - 克隆模板
*Clone() (Template, error)
说明:
- 创建模板的副本
- 用于创建模板变体
示例:
package main
import (
"html/template"
"os"
)
func main() {
base := template.Must(template.New("base").Parse("Base: {{.}}"))
// 克隆并修改
derived := template.Must(base.Clone())
derived.Parse(" Derived: {{.}}")
base.Execute(os.Stdout, "Test")
fmt.Println()
derived.Execute(os.Stdout, "Test")
}
DefinedNames - 获取已定义的模板名
DefinedNames() []string
说明:
- 返回所有已定义的模板名称
- 用于检查可用模板
示例:
package main
import (
"fmt"
"html/template"
)
func main() {
tmpl := template.Must(template.New("main").Parse(`
{{define "header"}}Header{{end}}
{{define "footer"}}Footer{{end}}
`))
names := tmpl.DefinedNames()
fmt.Printf("已定义的模板:%v\n", names)
}
运行:
$ go run main.go
已定义的模板:[main header footer]
Delims - 设置分隔符
*Delims(left, right string) Template
说明:
- 设置模板动作的分隔符
- 默认是
{{和}} - 用于避免与某些语法冲突
示例:
package main
import (
"html/template"
"os"
)
func main() {
// 使用 [[ 和 ]] 作为分隔符
tmpl := template.Must(template.New("test").Delims("[[", "]]").
Parse("Hello, [[.]]!"))
tmpl.Execute(os.Stdout, "World")
}
运行:
$ go run main.go
Hello, World!
Escape - 应用转义
Escape() error
说明:
- 手动应用转义
- 通常自动调用
Execute - 执行模板
Execute(wr io.Writer, data interface{}) error
说明:
- 将模板应用到数据并输出
- 如果出错会停止执行
- 模板可安全并发执行
定义:
func (t *Template) Execute(wr io.Writer, data interface{}) error
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := template.Must(template.New("test").Parse("Name: {{.Name}}, Age: {{.Age}}"))
data := struct {
Name string
Age int
}{
Name: "Alice",
Age: 25,
}
tmpl.Execute(os.Stdout, data)
}
运行:
$ go run main.go
Name: Alice, Age: 25
ExecuteTemplate - 执行命名模板
ExecuteTemplate(wr io.Writer, name string, data interface{}) error
说明:
- 执行指定名称的模板
- 用于执行嵌套或定义的模板
定义:
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
示例:
package main
import (
"html/template"
"os"
)
func main() {
tmpl := template.Must(template.New("main").Parse(`
{{define "greeting"}}Hello, {{.}}!{{end}}
{{define "farewell"}}Goodbye, {{.}}!{{end}}
`))
// 执行不同的模板
tmpl.ExecuteTemplate(os.Stdout, "greeting", "Alice")
fmt.Println()
tmpl.ExecuteTemplate(os.Stdout, "farewell", "Alice")
}
运行:
$ go run main.go
Hello, Alice!
Goodbye, Alice!
Funcs - 添加自定义函数
*Funcs(funcMap FuncMap) Template
说明:
- 向模板添加自定义函数
- 必须在 Parse 之前调用
- 返回模板以便链式调用
定义:
func (t *Template) Funcs(funcMap FuncMap) *Template
示例:
package main
import (
"html/template"
"os"
"strings"
)
func main() {
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"reverse": func(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
},
}
tmpl := template.Must(template.New("test").Funcs(funcMap).
Parse("{{.Name | upper | reverse}}"))
data := struct{ Name string }{"hello"}
tmpl.Execute(os.Stdout, data)
}
运行:
$ go run main.go
OLLEH
Lookup - 查找模板
*Lookup(name string) Template
说明:
- 查找指定名称的模板
- 如果不存在返回 nil
示例:
package main
import (
"fmt"
"html/template"
)
func main() {
tmpl := template.Must(template.New("main").Parse(`
{{define "header"}}Header{{end}}
`))
// 查找模板
if t := tmpl.Lookup("header"); t != nil {
fmt.Println("找到模板:header")
}
if t := tmpl.Lookup("footer"); t == nil {
fmt.Println("模板不存在:footer")
}
}
运行:
$ go run main.go
找到模板:header
模板不存在:footer
Name - 获取模板名
Name() string
说明:
- 返回模板的名称
示例:
package main
import (
"fmt"
"html/template"
)
func main() {
tmpl := template.Must(template.New("myTemplate").Parse("Content"))
fmt.Printf("模板名:%s\n", tmpl.Name())
}
运行:
$ go run main.go
模板名:myTemplate
Option - 设置选项
*Option(opt …string) Template
说明:
- 设置模板选项
- 如 “missingkey=error” 等
示例:
package main
import (
"html/template"
"os"
)
func main() {
// 设置缺失键的处理方式
tmpl := template.Must(template.New("test").
Option("missingkey=zero").
Parse("Value: {{.MissingKey}}"))
data := map[string]interface{}{}
tmpl.Execute(os.Stdout, data)
}
Parse - 解析模板字符串
*Parse(text string) (Template, error)
说明:
- 解析模板字符串
- 可多次调用以添加模板定义
- 只有第一次调用可以包含模板定义外的文本
定义:
func (t *Template) Parse(text string) (*Template, error)
示例:
package main
import (
"html/template"
"os"
)
func main() {
// 链式调用
tmpl := template.Must(template.New("base").
Parse("{{define "T1"}}T1 content{{end}}").
Parse("{{define "T2"}}T2 content{{end}}"))
tmpl.ExecuteTemplate(os.Stdout, "T1", nil)
fmt.Println()
tmpl.ExecuteTemplate(os.Stdout, "T2", nil)
}
运行:
$ go run main.go
T1 content
T2 content
ParseFiles - 解析文件
*ParseFiles(filenames …string) (Template, error)
说明:
- 解析一个或多个文件
- 作为 Template 方法时可向现有模板添加定义
示例:
package main
import (
"html/template"
"os"
)
func main() {
// 创建基础模板
tmpl := template.New("base")
// 添加文件定义
tmpl.ParseFiles("header.html", "footer.html")
tmpl.ExecuteTemplate(os.Stdout, "header", nil)
}
四、使用场景
场景 1:Web 页面渲染
package main
import (
"html/template"
"net/http"
)
type PageData struct {
Title string
Content string
User string
}
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("page.html"))
data := PageData{
Title: "My Page",
Content: "Welcome to my site!",
User: "Alice",
}
tmpl.Execute(w, data)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
场景 2:邮件模板
package main
import (
"bytes"
"html/template"
)
type EmailData struct {
Username string
Link string
}
func sendEmail(username, email string) {
tmplStr := `
<html>
<body>
<h1>Hello, {{.Username}}!</h1>
<p>Click <a href="{{.Link}}">here</a> to activate.</p>
</body>
</html>`
tmpl := template.Must(template.New("email").Parse(tmplStr))
data := EmailData{
Username: username,
Link: "https://example.com/activate?token=abc123",
}
var buf bytes.Buffer
tmpl.Execute(&buf, data)
// 发送邮件...
}
场景 3:代码生成
package main
import (
"html/template"
"os"
)
type APIData struct {
Name string
Params []string
}
func generateAPI(data APIData) {
tmplStr := `
func {{.Name}}({{range .Params}}{{.}}, {{end}}) error {
// Implementation here
return nil
}`
tmpl := template.Must(template.New("api").Parse(tmplStr))
tmpl.Execute(os.Stdout, data)
}
场景 4:布局模板
// layout.html
/*
{{define "header"}}
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
{{end}}
{{define "footer"}}
</body>
</html>
{{end}}
*/
// content.html
/*
{{template "header" .}}
<h1>{{.Heading}}</h1>
<p>{{.Content}}</p>
{{template "footer" .}}
*/
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("layout.html", "content.html"))
data := struct {
Title string
Heading string
Content string
}{
Title: "My Page",
Heading: "Welcome",
Content: "Hello, World!",
}
tmpl.ExecuteTemplate(w, "content", data)
}
五、最佳实践
1. 始终使用 html/template 处理 HTML
// 推荐:使用 html/template
import "html/template"
tmpl := template.Must(template.New("test").Parse("{{.}}"))
// 不推荐:使用 text/template 处理 HTML
import "text/template" // 不会自动转义!
2. 使用 Must 简化初始化
// 推荐:使用 Must
var tmpl = template.Must(template.ParseFiles("index.html"))
// 不推荐:每次都检查错误
tmpl, err := template.ParseFiles("index.html")
if err != nil {
panic(err)
}
3. 预编译模板
// 推荐:包变量预编译
var templates = template.Must(template.ParseGlob("templates/*.html"))
func handler(w http.ResponseWriter, r *http.Request) {
templates.ExecuteTemplate(w, "index", data)
}
// 不推荐:每次都解析
func handler(w http.ResponseWriter, r *http.Request) {
tmpl, _ := template.ParseFiles("index.html")
tmpl.Execute(w, data)
}
4. 使用自定义类型标记安全内容
// 已知安全的 HTML
data := struct {
SafeHTML template.HTML
}{
SafeHTML: template.HTML("<b>Bold</b>"),
}
// 不要直接使用 string
data := struct {
Content string // 会被转义
}{
Content: "<b>Bold</b>", // 输出 <b>Bold</b>
}
5. 自定义函数要安全
// 安全的自定义函数
funcMap := template.FuncMap{
"safeFunc": func(s string) string {
return strings.ToUpper(s) // 安全
},
}
// 不安全的自定义函数
funcMap := template.FuncMap{
"unsafeFunc": func(s string) template.HTML {
return template.HTML(s) // 危险!可能引入 XSS
},
}
六、快速参考
核心类型
| 类型 | 说明 | 用途 |
|---|---|---|
| Template | 模板类型 | 表示已解析的模板 |
| FuncMap | 函数映射 | 自定义模板函数 |
| HTML | 安全 HTML | 不转义的 HTML 字符串 |
| HTMLAttr | 安全属性 | HTML 属性名 |
| JS | 安全 JS | JavaScript 代码 |
| JSStr | 安全 JS 字符串 | JS 中的字符串 |
| URL | 安全 URL | URL 地址 |
| Srcset | 安全 Srcset | img srcset 属性 |
核心函数
| 函数 | 说明 | 示例 |
|---|---|---|
| New(name) | 创建模板 | template.New("test") |
| Must(t, err) | 错误处理 | template.Must(Parse(...)) |
| ParseFiles(…) | 解析文件 | template.ParseFiles("a.html") |
| ParseGlob(pattern) | 解析匹配文件 | template.ParseGlob("*.html") |
Template 方法
| 方法 | 说明 | 示例 |
|---|---|---|
| Parse(text) | 解析字符串 | tmpl.Parse("{{.}}") |
| Execute(w, data) | 执行模板 | tmpl.Execute(os.Stdout, data) |
| ExecuteTemplate(w, name, data) | 执行命名模板 | tmpl.ExecuteTemplate(w, "name", data) |
| Funcs(funcMap) | 添加函数 | tmpl.Funcs(funcMap) |
| Clone() | 克隆模板 | tmpl.Clone() |
| Delims(left, right) | 设置分隔符 | tmpl.Delims("[[", "]]") |
| DefinedNames() | 获取模板名列表 | tmpl.DefinedNames() |
| Lookup(name) | 查找模板 | tmpl.Lookup("name") |
上下文转义
| 上下文 | 转义方式 | 示例 |
|---|---|---|
| HTML 正文 | HTML 转义 | < > & ' " → 实体 |
| HTML 属性 | 属性转义 | ' " → 实体 |
| URL | URL 编码 | 特殊字符 → %XX |
| JavaScript | JS 转义 | 引号、换行 → 转义 |
| CSS | CSS 转义 | 特殊字符 → 转义 |
安全类型
| 类型 | 转义 | 使用场景 |
|---|---|---|
| string | 是 | 普通文本 |
| template.HTML | 否 | 已知安全的 HTML |
| template.URL | 否 | 已知安全的 URL |
| template.JS | 否 | 已知安全的 JS |
七、与其他包配合
与 net/http 配合
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("index.html"))
tmpl.Execute(w, struct{ Name string }{"Alice"})
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
与 bytes 配合
package main
import (
"bytes"
"fmt"
"html/template"
)
func renderTemplate(tmplStr string, data interface{}) (string, error) {
tmpl := template.Must(template.New("test").Parse(tmplStr))
var buf bytes.Buffer
err := tmpl.Execute(&buf, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func main() {
result, _ := renderTemplate("Hello, {{.Name}}!", struct{ Name string }{"Alice"})
fmt.Println(result)
}
与 strings 配合
package main
import (
"html/template"
"os"
"strings"
)
func main() {
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"split": strings.Split,
"join": strings.Join,
}
tmpl := template.Must(template.New("test").Funcs(funcMap).
Parse("{{.Text | upper}}"))
data := struct{ Text string }{"hello"}
tmpl.Execute(os.Stdout, data)
}
八、注意事项
1. 自动转义是上下文相关的
// HTML 上下文
tmpl := `<div>{{.}}</div>`
// 输出:<div><script>alert('XSS')</script></div>
// URL 上下文
tmpl := `<a href="{{.}}">Link</a>`
// 如果 . 是 "javascript:alert('XSS')",输出会被过滤为 "#ZgotmplZ"
2. 不要滥用安全类型
// 危险:直接使用 template.HTML
data := template.HTML(userInput) // 危险!
// 安全:先验证和清理
if isValidHTML(userInput) {
data := template.HTML(userInput)
}
3. 模板作者必须是可信的
// html/template 假设模板作者是可信的
// 数据参数是不可信的
// 所以不要让用户上传或修改模板
4. Funcs 必须在 Parse 之前调用
// 正确
tmpl.Funcs(funcMap).Parse("...")
// 错误:panic
tmpl.Parse("...").Funcs(funcMap)
5. 模板可并发执行
// 安全:同一个模板可被多个 goroutine 并发执行
tmpl := template.Must(template.ParseFiles("index.html"))
go func() { tmpl.Execute(os.Stdout, data1) }()
go func() { tmpl.Execute(os.Stdout, data2) }()
九、常见问题
Q1: html/template 和 text/template 有什么区别?
A:
html/template会自动根据上下文转义输出,防止 XSStext/template不会转义,适合生成纯文本- 生成 HTML 时应始终使用
html/template
Q2: 如何禁用自动转义?
A:
- 使用安全类型(template.HTML、template.URL 等)标记已知安全的内容
- 不要完全禁用转义,这会带来安全风险
Q3: 如何处理富文本编辑器内容?
A:
- 使用 HTML 清理库(如 bluemonday)
- 清理后使用 template.HTML 标记
- 在模板中输出
最后更新:2026-04-04
Go 版本:Go 1.23+