html - HTML 文本转义
html 包提供了 HTML 文本的转义和反转义功能,用于安全地处理 HTML 内容。
概述
html 包是 Go 标准库中用于处理 HTML 文本转义的基础包,主要提供两个函数:EscapeString 用于转义 HTML 特殊字符,UnescapeString 用于还原 HTML 实体。这些函数在生成 HTML 内容、防止 XSS 攻击等场景中非常有用。
包导入:
import "html"
基本使用:
// 1. 转义 HTML 特殊字符
escaped := html.EscapeString("<script>alert('XSS')</script>")
// 2. 反转义 HTML 实体
unescaped := html.UnescapeString("<hello>")
典型示例:
示例 1:基本转义:
package main
import (
"fmt"
"html"
)
func main() {
// 原始字符串
original := `<script>alert("XSS")</script>`
// 转义
escaped := html.EscapeString(original)
fmt.Printf("转义后:%s\n", escaped)
// 反转换
unescaped := html.UnescapeString(escaped)
fmt.Printf("反转换后:%s\n", unescaped)
// 验证
fmt.Printf("原始 == 反转换:%v\n", original == unescaped)
}
运行:
$ go run main.go
转义后:<script>alert("XSS")</script>
反转换后:<script>alert("XSS")</script>
原始 == 反转换:true
示例 2:用户输入安全显示:
package main
import (
"fmt"
"html"
"strings"
)
// 安全地显示用户评论
func displayComment(userInput string) string {
// 转义 HTML 特殊字符,防止 XSS
safe := html.EscapeString(userInput)
return fmt.Sprintf("<div class='comment'>%s</div>", safe)
}
func main() {
// 恶意用户输入
malicious := "<script>alert('hacked')</script>"
normal := "Hello, World!"
fmt.Println("恶意输入处理:")
fmt.Println(displayComment(malicious))
fmt.Println("\n正常输入处理:")
fmt.Println(displayComment(normal))
// 在 HTML 中安全显示
fmt.Println("\n完整 HTML:")
var sb strings.Builder
sb.WriteString("<html><body>")
sb.WriteString(displayComment(malicious))
sb.WriteString("</body></html>")
fmt.Println(sb.String())
}
运行:
$ go run main.go
恶意输入处理:
<div class='comment'><script>alert('hacked')</script></div>
正常输入处理:
<div class='comment'>Hello, World!</div>
完整 HTML:
<html><body><div class='comment'><script>alert('hacked')</script></div></body></html>
示例 3:处理各种 HTML 实体:
package main
import (
"fmt"
"html"
)
func main() {
// 各种 HTML 实体
entities := []string{
"<hello>", // <hello>
"&and&", // &and&
""quoted"", // "quoted"
"'single'", // 'single'
"á", // á
"á", // á (十进制)
"á", // á (十六进制)
}
fmt.Println("反转换 HTML 实体:")
for _, entity := range entities {
unescaped := html.UnescapeString(entity)
fmt.Printf("%-20s -> %s\n", entity, unescaped)
}
fmt.Println("\n转义特殊字符:")
special := `<>&'"`
escaped := html.EscapeString(special)
fmt.Printf("原始:%s\n", special)
fmt.Printf("转义:%s\n", escaped)
}
运行:
$ go run main.go
反转换 HTML 实体:
<hello> -> <hello>
&and& -> &and&
"quoted" -> "quoted"
'single' -> 'single'
á -> á
á -> á
á -> á
转义特殊字符:
原始:<>&'"
转义:<>&'"
一、核心函数(按字母顺序)
EscapeString - 转义 HTML 字符串
EscapeString(s string) string
说明:
- 转义 HTML 特殊字符
- 只转义 5 个字符:
<、>、&、'、" - 将
<转为< - 将
>转为> - 将
&转为& - 将
'转为' - 将
"转为" UnescapeString(EscapeString(s)) == s总是成立
定义:
func EscapeString(s string) string
参数:
s:要转义的字符串
返回值:
string:转义后的字符串
示例:
package main
import (
"fmt"
"html"
)
func main() {
// 转义 5 个特殊字符
tests := []string{
"<script>",
"a > b",
"Tom & Jerry",
"It's fine",
"He said \"Hello\"",
}
for _, test := range tests {
escaped := html.EscapeString(test)
fmt.Printf("原始:%-25s 转义:%s\n", test, escaped)
}
// 验证可逆性
original := "<>&'\""
escaped := html.EscapeString(original)
unescaped := html.UnescapeString(escaped)
fmt.Printf("\n原始:%s\n", original)
fmt.Printf("转义:%s\n", escaped)
fmt.Printf("反转换:%s\n", unescaped)
fmt.Printf("原始 == 反转换:%v\n", original == unescaped)
}
运行:
$ go run main.go
原始:<script> 转义:<script>
原始:a > b 转义:a > b
原始:Tom & Jerry 转义:Tom & Jerry
原始:It's fine 转义:It's fine
原始:He said "Hello" 转义:He said "Hello"
原始:<>&'"
转义:<>&'"
反转换:<>&'"
原始 == 反转换:true
UnescapeString - 反转换 HTML 字符串
UnescapeString(s string) string
说明:
- 反转换 HTML 实体为原始字符
- 支持的实体范围比
EscapeString转义的范围更广 - 支持命名实体(如
á→á) - 支持十进制实体(如
á→á) - 支持十六进制实体(如
á→á) UnescapeString(EscapeString(s)) == s总是成立,但反过来不一定成立
定义:
func UnescapeString(s string) string
参数:
s:包含 HTML 实体的字符串
返回值:
string:反转换后的字符串
示例:
package main
import (
"fmt"
"html"
)
func main() {
// 各种 HTML 实体
tests := map[string]string{
"<": "<",
">": ">",
"&": "&",
"'": "'",
""": "\"",
"á": "á",
"©": "©",
"®": "®",
"™": "™",
" ": " ",
"á": "á",
"á": "á",
""": "\"",
}
fmt.Println("HTML 实体反转换:")
for entity, expected := range tests {
result := html.UnescapeString(entity)
match := "✓"
if result != expected {
match = "✗"
}
fmt.Printf("%-15s -> %-5s (期望:%s) %s\n", entity, result, expected, match)
}
// 复杂示例
complex := ""Fran & Freddie's Diner" <tasty@example.com>"
fmt.Printf("\n复杂示例:\n")
fmt.Printf("转义:%s\n", complex)
fmt.Printf("原始:%s\n", html.UnescapeString(complex))
}
运行:
$ go run main.go
HTML 实体反转换:
< -> < (期望:<) ✓
> -> > (期望:>) ✓
& -> & (期望:&) ✓
' -> ' (期望:') ✓
" -> " (期望:") ✓
á -> á (期望:á) ✓
© -> © (期望:©) ✓
® -> ® (期望:®) ✓
™ -> ™ (期望:™) ✓
-> (期望: ) ✓
á -> á (期望:á) ✓
á -> á (期望:á) ✓
" -> " (期望:") ✓
复杂示例:
转义:"Fran & Freddie's Diner" <tasty@example.com>
原始:"Fran & Freddie's Diner" <tasty@example.com>
二、使用场景
场景 1:防止 XSS 攻击
package main
import (
"fmt"
"html"
"net/http"
)
// 安全的 HTTP 响应处理器
func safeHandler(w http.ResponseWriter, r *http.Request) {
// 获取用户输入
userInput := r.URL.Query().Get("name")
// 转义后输出,防止 XSS
safeName := html.EscapeString(userInput)
fmt.Fprintf(w, "<h1>Hello, %s!</h1>", safeName)
}
func main() {
http.HandleFunc("/greet", safeHandler)
fmt.Println("服务器启动在 :8080")
http.ListenAndServe(":8080", nil)
}
使用示例:
# 正常请求
$ curl "http://localhost:8080/greet?name=Alice"
<h1>Hello, Alice!</h1>
# 恶意请求(脚本会被转义,不会执行)
$ curl "http://localhost:8080/greet?name=<script>alert('XSS')</script>"
<h1>Hello, <script>alert('XSS')</script>!</h1>
场景 2:安全地显示用户评论
package main
import (
"fmt"
"html"
"strings"
)
type Comment struct {
Author string
Content string
}
// 安全地渲染评论
func renderComment(comment Comment) string {
var sb strings.Builder
sb.WriteString("<div class='comment'>\n")
sb.WriteString(" <div class='author'>")
sb.WriteString(html.EscapeString(comment.Author))
sb.WriteString("</div>\n")
sb.WriteString(" <div class='content'>")
sb.WriteString(html.EscapeString(comment.Content))
sb.WriteString("</div>\n")
sb.WriteString("</div>\n")
return sb.String()
}
func main() {
comments := []Comment{
{"Alice", "Great article!"},
{"Bob", "<script>alert('spam')</script>"},
{"Charlie", "Tom & Jerry's show"},
}
for _, comment := range comments {
fmt.Println(renderComment(comment))
}
}
运行:
$ go run main.go
<div class='comment'>
<div class='author'>Alice</div>
<div class='content'>Great article!</div>
</div>
<div class='comment'>
<div class='author'>Bob</div>
<div class='content'><script>alert('spam')</script></div>
</div>
<div class='comment'>
<div class='author'>Charlie</div>
<div class='content'>Tom & Jerry's show</div>
</div>
场景 3:生成安全的 HTML 属性
package main
import (
"fmt"
"html"
)
// 生成安全的 HTML 标签
func generateLink(href, text string) string {
return fmt.Sprintf("<a href='%s'>%s</a>",
html.EscapeString(href),
html.EscapeString(text))
}
func generateImage(src, alt string) string {
return fmt.Sprintf("<img src='%s' alt='%s'>",
html.EscapeString(src),
html.EscapeString(alt))
}
func main() {
// 正常链接
fmt.Println(generateLink("https://example.com", "Example"))
// 恶意链接(尝试注入 JavaScript)
fmt.Println(generateLink("javascript:alert('XSS')", "Click me"))
// 图片
fmt.Println(generateImage("/images/photo.jpg", "A beautiful photo"))
// 恶意 alt 文本
fmt.Println(generateImage("/images/photo.jpg", "' onerror='alert('XSS')"))
}
运行:
$ go run main.go
<a href='https://example.com'>Example</a>
<a href='javascript:alert('XSS')'>Click me</a>
<img src='/images/photo.jpg' alt='A beautiful photo'>
<img src='/images/photo.jpg' alt='' onerror='alert('XSS')'/>
场景 4:处理 XML/HTML 数据
package main
import (
"fmt"
"html"
)
// 解析并显示 XML/HTML 内容
func processContent(content string) {
fmt.Println("原始内容:")
fmt.Println(content)
fmt.Println()
// 如果需要显示原始内容(不解析)
fmt.Println("安全显示:")
fmt.Println(html.EscapeString(content))
fmt.Println()
// 如果内容包含需要解析的实体
fmt.Println("解析实体:")
fmt.Println(html.UnescapeString(content))
}
func main() {
// 混合内容
content := `<div>Hello & Welcome!</div>`
processContent(content)
// 包含特殊字符的内容
content2 := `<div>Tom & Jerry's "adventure"</div>`
processContent(content2)
}
运行:
$ go run main.go
原始内容:
<div>Hello & Welcome!</div>
安全显示:
&lt;div&gt;Hello &amp; Welcome!&lt;/div&gt;
解析实体:
<div>Hello & Welcome!</div>
原始内容:
<div>Tom & Jerry's "adventure"</div>
安全显示:
<div>Tom & Jerry's "adventure"</div>
解析实体:
<div>Tom & Jerry's "adventure"</div>
场景 5:日志记录中的安全处理
package main
import (
"fmt"
"html"
"time"
)
// 安全地记录用户输入日志
func logUserInput(action, userInput string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
// 转义后记录,防止日志查看器注入
safeInput := html.EscapeString(userInput)
fmt.Printf("[%s] %s: %s\n", timestamp, action, safeInput)
}
func main() {
// 正常操作
logUserInput("LOGIN", "user@example.com")
// 恶意输入
logUserInput("SEARCH", "<script>document.cookie</script>")
// 特殊字符
logUserInput("COMMENT", "Tom & Jerry <tom@example.com>")
}
运行:
$ go run main.go
[2026-04-04 10:30:00] LOGIN: user@example.com
[2026-04-04 10:30:01] SEARCH: <script>document.cookie</script>
[2026-04-04 10:30:02] COMMENT: Tom & Jerry <tom@example.com>
三、最佳实践
1. 始终转义用户输入
// 推荐:始终转义用户输入
func renderUserInput(input string) string {
return html.EscapeString(input)
}
// 不推荐:直接使用未转义的输入
func renderUserInput(input string) string {
return input // 危险!
}
2. 在 HTML 上下文中使用
// 在 HTML 内容中
func renderHTML(name string) string {
return fmt.Sprintf("<div>%s</div>", html.EscapeString(name))
}
// 在 HTML 属性中
func renderAttribute(value string) string {
return fmt.Sprintf("value='%s'", html.EscapeString(value))
}
// 在 JavaScript 中(需要额外处理)
// 注意:html.EscapeString 不足以防止 JS 注入
3. 组合使用转义和模板
package main
import (
"html"
"html/template"
"strings"
)
// 使用 template 包(更安全)
func renderWithTemplate(name string) string {
tmpl := `<div>Hello, {{.}}!</div>`
t := template.Must(template.New("greet").Parse(tmpl))
var sb strings.Builder
t.Execute(&sb, name) // template 会自动转义
return sb.String()
}
// 手动转义(当不能使用 template 时)
func renderManual(name string) string {
return "<div>Hello, " + html.EscapeString(name) + "!</div>"
}
4. 处理富文本内容
package main
import (
"html"
"regexp"
)
// 允许特定 HTML 标签的富文本处理
func sanitizeRichText(input string) string {
// 1. 转义所有 HTML
escaped := html.EscapeString(input)
// 2. 恢复允许的标签(如 <b>, <i>)
allowedTags := map[string]string{
"<b>": "<b>",
"</b>": "</b>",
"<i>": "<i>",
"</i>": "</i>",
}
result := escaped
for escapedTag, originalTag := range allowedTags {
result = regexp.MustCompile(regexp.QuoteMeta(escapedTag)).ReplaceAllString(result, originalTag)
}
return result
}
5. 性能优化
// 批量处理时复用转义结果
func processComments(comments []string) []string {
results := make([]string, len(comments))
// 缓存已转义的内容(如果可能重复)
cache := make(map[string]string)
for i, comment := range comments {
if escaped, ok := cache[comment]; ok {
results[i] = escaped
} else {
escaped := html.EscapeString(comment)
cache[comment] = escaped
results[i] = escaped
}
}
return results
}
四、快速参考
核心函数
| 函数 | 说明 | 转义字符 | 示例 |
|---|---|---|---|
| EscapeString(s) | 转义 HTML 特殊字符 | < > & ' " | html.EscapeString("<>") → <> |
| UnescapeString(s) | 反转换 HTML 实体 | 所有 HTML 实体 | html.UnescapeString("<") → < |
转义字符对照表
| 字符 | 转义后 | 说明 |
|---|---|---|
< | < | 小于号 / 标签开始 |
> | > | 大于号 / 标签结束 |
& | & | 和号 / 实体开始 |
' | ' | 单引号 |
" | " | 双引号 |
支持的 HTML 实体类型
| 类型 | 示例 | 说明 |
|---|---|---|
| 命名实体 | & < © | 预定义的实体名称 |
| 十进制实体 | ' á | &# + 十进制数字 |
| 十六进制实体 | ' á | &#x + 十六进制数字 |
使用场景
| 场景 | 推荐函数 | 说明 |
|---|---|---|
| 显示用户输入 | EscapeString | 防止 XSS 攻击 |
| 生成 HTML 属性 | EscapeString | 防止属性注入 |
| 解析 HTML 实体 | UnescapeString | 还原原始文本 |
| 日志记录 | EscapeString | 防止日志注入 |
| 处理 XML/HTML | 两者结合 | 根据需求选择 |
与 template 包配合
| 包 | 用途 | 自动转义 |
|---|---|---|
| html | 手动转义 | 否 |
| html/template | 模板渲染 | 是(推荐) |
五、与其他包配合
与 fmt 包配合
package main
import (
"fmt"
"html"
)
func main() {
name := "<script>alert('XSS')</script>"
// 错误:直接输出
fmt.Printf("错误:<div>%s</div>\n", name)
// 正确:转义后输出
fmt.Printf("正确:<div>%s</div>\n", html.EscapeString(name))
}
运行:
$ go run main.go
错误:<div><script>alert('XSS')</script></div>
正确:<div><script>alert('XSS')</script></div>
与 strings 包配合
package main
import (
"fmt"
"html"
"strings"
)
func main() {
userInput := []string{
"<script>",
"Tom & Jerry",
"\"Hello\"",
}
// 批量转义
var sb strings.Builder
sb.WriteString("<ul>\n")
for _, input := range userInput {
sb.WriteString(" <li>")
sb.WriteString(html.EscapeString(input))
sb.WriteString("</li>\n")
}
sb.WriteString("</ul>\n")
fmt.Println(sb.String())
}
运行:
$ go run main.go
<ul>
<li><script></li>
<li>Tom & Jerry</li>
<li>"Hello"</li>
</ul>
与 net/http 包配合
package main
import (
"html"
"net/http"
)
func main() {
http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// 转义后显示
fmt.Fprintf(w, "<h1>搜索结果:%s</h1>", html.EscapeString(query))
})
http.ListenAndServe(":8080", nil)
}
与 html/template 包配合
package main
import (
"html"
"html/template"
"os"
)
func main() {
// 手动转义(当需要精细控制时)
manual := html.EscapeString("<script>")
// 使用 template(推荐,自动转义)
tmpl := template.Must(template.New("test").Parse("{{.}}"))
tmpl.Execute(os.Stdout, "<script>") // 自动转义
}
六、注意事项
1. EscapeString 只转义 5 个字符
// EscapeString 只转义:< > & ' "
input := "<>&'\""
escaped := html.EscapeString(input)
fmt.Println(escaped) // <>&'"
// 其他字符不会转义
input2 := "你好,世界!\n\r\t"
escaped2 := html.EscapeString(input2)
fmt.Println(escaped2) // 你好,世界!\n\r\t (不变)
2. UnescapeString 支持更多实体
// 命名实体
fmt.Println(html.UnescapeString("©")) // ©
fmt.Println(html.UnescapeString("®")) // ®
// 十进制实体
fmt.Println(html.UnescapeString("©")) // ©
// 十六进制实体
fmt.Println(html.UnescapeString("©")) // ©
3. 不适用于 JavaScript 上下文
// 错误:在 JavaScript 中使用 html.EscapeString
// <script>var x = "{{.}}";</script>
// 即使转义了引号,仍可能有其他注入方式
// 正确:使用专门的 JS 转义或 template.JS
4. 转义是单向的(信息丢失)
// 多个不同的原始字符串可能转义为相同结果
s1 := "&"
s2 := "&"
escaped1 := html.EscapeString(s1) // &amp;
escaped2 := html.EscapeString(s2) // &
// UnescapeString 后
fmt.Println(html.UnescapeString(escaped1)) // &
fmt.Println(html.UnescapeString(escaped2)) // &
5. 性能考虑
// 对于大量文本,转义操作有开销
// 建议:
// 1. 缓存已转义的内容
// 2. 使用 html/template 自动处理
// 3. 避免重复转义同一内容
七、常见问题
Q1: EscapeString 和 html/template 有什么区别?
A:
html.EscapeString是手动转义,需要开发者显式调用html/template是自动转义,在模板渲染时自动处理- 推荐使用
html/template生成 HTML,更安全方便
Q2: 为什么 UnescapeString(EscapeString(s)) == s 总是成立,但反过来不一定?
A:
EscapeString只转义 5 个字符,信息不丢失UnescapeString支持更多实体(如©→©)- 所以
UnescapeString("©")→©,但EscapeString("©")→©(不变)
Q3: 如何处理富文本(允许部分 HTML 标签)?
A:
- 先用
EscapeString转义所有内容 - 再用正则恢复允许的标签
- 或使用专门的 HTML 清理库(如 bluemonday)
最后更新:2026-04-04
Go 版本:Go 1.23+