登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go text/template与html/template选择输出边界的对比

来源:17golang原创

时间:2026-09-23 13:13:29 275浏览 收藏

我第一次把 Go 模板接进一个小型管理页时,真正容易混淆的不是 {{.Title}} 怎么写,而是“这段输出最终给谁消费”。日志、邮件正文和配置文件需要保留文本;浏览器 HTML 则必须让数据留在正确的 HTML、URI 或 JavaScript 上下文里。两套包的解析和执行 API 很像,选择却不能只看调用习惯。

结论很明确:输出是纯文本时使用 text/template;输出会进入 HTML 页面时使用 html/template。后者会根据上下文自动转义普通数据,不能用前者渲染网页,也不要把手动转义当成替代方案。

这篇用同一份数据做一个最小对比项目,重点看选择依据、代码差异、转义位置和可信 HTML 的边界。

项目目标:先按输出边界做选择

先不要急着把两个包抽象成同一个渲染器。我会先写一张输出清单:命令行日志、邮件纯文本、Markdown 或配置片段属于文本输出;服务端生成的网页属于 HTML 输出。前一组可以使用 text/template,后一组直接使用 html/template

最终消费者选择关键理由
日志、邮件、文本文件text/template模板包把动作结果按文本写出,不主动做 HTML 上下文转义
浏览器 HTMLhtml/template按 HTML、CSS、JavaScript、URI 上下文处理普通数据
受信任的 HTML 片段谨慎使用 template.HTML只在片段来源和拼接过程都由受信任代码控制时保留标记
text/template与html/template输出边界结构说明图
图1:输出边界结构说明图,静态展示数据、模板包与纯文本/HTML消费者的关系,不是运行截图。

核心代码:分别解析并执行两种模板

下面的示例让两种模板共享数据结构和模板内容,只替换 import 路径。代码中的错误不能省略:Parse 负责模板语法,Execute 负责把数据写入目标;两处失败都应该交给调用方处理。

package main

import (
    "bytes"
    "fmt"
    htmltmpl "html/template"
    texttmpl "text/template"
)

type PageData struct {
    Title string
    Link  string
}

const pageTemplate = `

{{.Title}}

查看详情` func renderText(data PageData) (string, error) { // 纯文本输出不承担 HTML 上下文安全职责,适合日志或邮件正文。 tmpl, err := texttmpl.New("text").Parse(pageTemplate) if err != nil { return "", fmt.Errorf("解析文本模板: %w", err) } var buf bytes.Buffer // Execute 失败时返回部分结果也不能被当成完整页面继续使用。 if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("执行文本模板: %w", err) } return buf.String(), nil } func renderHTML(data PageData) (string, error) { // HTML 输出交给 html/template,根据值所在的上下文做转义。 tmpl, err := htmltmpl.New("html").Parse(pageTemplate) if err != nil { return "", fmt.Errorf("解析 HTML 模板: %w", err) } var buf bytes.Buffer // 保留错误返回,让上层决定记录、重试还是返回 HTTP 错误。 if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("执行 HTML 模板: %w", err) } return buf.String(), nil }

实际 Go 源文件中的模板字符串应使用原始反引号,并把 HTML 标签直接写入字符串;上面为了让文章页面保留标签,展示时对尖括号做了 HTML 实体编码。两种函数的关键差异只有包路径,但这正是安全语义的分界。

本地运行:用同一份数据观察转义结果

测试数据要刻意包含标签字符和一个带查询参数的链接。不要把“页面看起来能打开”当作结论,应分别观察文本节点和属性值。

func main() {
    data := PageData{
        // 用特殊字符验证普通文本与 HTML 上下文的差异。
        Title: `内部公告`,
        Link:  `/search?q=go&mode=full`,
    }

    plain, err := renderText(data)
    if err != nil {
        panic(err)
    }
    page, err := renderHTML(data)
    if err != nil {
        panic(err)
    }
    fmt.Println("text/template:", plain)
    fmt.Println("html/template:", page)
}

text/template 中,数据更接近原样写入;如果这段结果直接作为 HTML 响应,数据中的标记就可能改变页面结构。html/template 会把标题中的尖括号编码成文本,并在 href 这样的 URI/属性上下文中应用对应处理。它不是“把所有字符无脑替换”,而是根据模板位置计算上下文。

HTML文本节点与href属性上下文转义关系说明图
图2:上下文转义关系说明图,静态展示普通数据进入 HTML 文本节点与 href 属性后的边界,不是运行结果截图。

验收结论:把 HTML、纯文本和可信片段分开

最后我会把选择规则写进代码评审清单。第一,模板作者可以是可信的,但 Execute 接收的数据默认不可信;第二,网页输出统一从 html/template 开始,不用 text/template 生成 HTML;第三,template.HTML 是信任承诺,不是“关闭转义”的快捷按钮。

只有在 HTML 片段由受信任代码构造、结构经过明确控制,而且调用方确实需要保留标签时,才考虑类型化的安全内容。用户输入、数据库字段或外部接口返回值不要直接转换成 template.HTML。同样,html/template 中也不必手动套 htmlurlquery 来“加固”,让包根据上下文处理更容易维护。

速记:文本消费者选 text/template,HTML 消费者选 html/template;先判断输出边界,再判断数据是否可信,最后才考虑模板函数或安全类型。

相关问题

两个包的模板语法能共用吗

大多数基础动作和执行接口相同,因此可以共用不少模板文本,但最终输出格式和安全职责不同,不能因为语法相同就互换包。

为什么 html/template 输出里出现了转义实体

普通字符串被当作数据而不是模板作者写入的标记。转成实体后,浏览器会把它显示为文本,从而保持 HTML 结构不被数据改变。

什么时候可以使用 template.HTML

仅限于来源、拼接逻辑和允许的标签都由受信任代码控制的片段;不应把外部输入通过类型转换直接标记为安全 HTML。

参考资料:https://pkg.go.dev/text/templatehttps://pkg.go.dev/html/templatehttps://go.dev/src/html/template/doc.go

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>