不是所有输出都需要 HTML。很多内部工具会生成纯文本日报、邮件正文、Markdown 摘要、配置片段或工单说明。直接用 fmt.Fprintf 拼字符串可以起步,但字段一多,换行和缩进就会变得难维护。Go 标准库的 text/template 很适合这类场景。
text/template 和 html/template 语法相近,但前者不会做 HTML 转义。生成纯文本、Markdown、SQL 片段时用它;生成网页时优先用 html/template。
第一份日报
定义数据:
type Report struct {
Date string
Total int
Success int
Failed int
}
模板:
const dailyTemplate = `日报 {{.Date}}
总任务:{{.Total}}
成功:{{.Success}}
失败:{{.Failed}}
`
渲染:
func renderReport(w io.Writer, r Report) error {
tmpl, err := template.New("daily").Parse(dailyTemplate)
if err != nil {
return err
}
return tmpl.Execute(w, r)
}
模板里的 {{.Date}} 表示访问传入数据的字段。字段必须是导出的,也就是首字母大写。date 这种小写字段模板访问不到。
循环列表
日报通常要列出失败项:
type FailedItem struct {
ID string
Reason string
}
type Report struct {
Date string
Items []FailedItem
}
模板中使用 range:
const tpl = `失败列表:
{{range .Items}}- {{.ID}}:{{.Reason}}
{{end}}`
如果列表为空,输出会只剩标题。可以用 else:
const tpl = `失败列表:
{{range .Items}}- {{.ID}}:{{.Reason}}
{{else}}无失败任务
{{end}}`
这个语法很适合报表。业务代码不用专门判断空列表,模板自己决定展示文字。
条件判断
模板支持 if:
const tpl = `{{if .HasError}}状态:需要处理{{else}}状态:正常{{end}}`
但不要把复杂业务逻辑放进模板。比如“失败率超过 5% 且 VIP 客户超过 3 个时升级告警”,这种判断应该在 Go 代码里算好,模板只展示结果。
type Report struct {
NeedAttention bool
Summary string
}
模板越像展示层,越好维护。不要让模板变成另一种难调试的业务语言。
自定义函数
需要格式化数字或时间时,可以注册函数:
func percent(n, total int) string {
if total == 0 {
return "0%"
}
return fmt.Sprintf("%.1f%%", float64(n)*100/float64(total))
}
func newReportTemplate() (*template.Template, error) {
funcs := template.FuncMap{
"percent": percent,
}
return template.New("report").Funcs(funcs).Parse(`失败率:{{percent .Failed .Total}}`)
}
函数要在 Parse 前注册。函数里尽量不要访问数据库、网络或全局状态。它应该像格式化工具,而不是业务服务。
空白控制
模板里的换行和空格会原样输出。Go 模板支持 {{- 和 -}} 控制空白:
const tpl = `
{{- range .Items }}
- {{ .ID }}
{{- end }}
`
空白控制很有用,但不要过度使用。模板本来就不如 Go 代码容易调试,太多短横线会降低可读性。生成 Markdown 时,适当保留空行反而更清楚。
从文件加载模板
模板长了以后,放在单独文件更合适。比如 templates/daily.txt:
日报 {{.Date}}
{{range .Items}}- {{.ID}} {{.Reason}}
{{else}}今天没有失败任务。
{{end}}
加载:
tmpl, err := template.ParseFiles("templates/daily.txt")
if err != nil {
return err
}
err = tmpl.ExecuteTemplate(w, "daily.txt", report)
如果要把工具打成单个二进制,可以配合 embed。纯文本模板也可以嵌入:
//go:embed templates/*.txt
var templateFS embed.FS
tmpl, err := template.ParseFS(templateFS, "templates/*.txt")
这样部署时不用担心忘记带模板文件。
错误处理
Parse 和 Execute 都要处理错误。Parse 错通常是模板语法错误,应该在启动或测试阶段发现。Execute 错可能是字段不存在、函数返回错误或写入失败。
var buf bytes.Buffer
if err := tmpl.Execute(&buf, report); err != nil {
return fmt.Errorf("execute report template: %w", err)
}
先写入 buffer,再把结果写到文件或 HTTP 响应,能避免输出一半才失败。对邮件正文和报表文件来说,这种做法很实用。
测试输出
模板输出适合做快照式测试,但不要让测试过于脆弱。可以检查关键片段:
func TestRenderReport(t *testing.T) {
var buf bytes.Buffer
err := renderReport(&buf, Report{
Date: "2025-12-05",
Items: []FailedItem{{ID: "job-1", Reason: "timeout"}},
})
if err != nil {
t.Fatal(err)
}
got := buf.String()
if !strings.Contains(got, "job-1") {
t.Fatalf("missing job id: %s", got)
}
if !strings.Contains(got, "timeout") {
t.Fatalf("missing reason: %s", got)
}
}
如果报表格式要求严格,比如要发给外部系统解析,可以比较完整字符串。内部日报则检查关键字段更稳,避免因为多一个空行就频繁改测试。
text/template 和 html/template
两者不要混用。html/template 会根据上下文自动转义,适合 HTML 页面。text/template 不转义,适合纯文本。如果用 text/template 生成 HTML,用户输入里带 <script> 就可能原样输出,造成 XSS 风险。
反过来,如果你用 html/template 生成 Markdown,某些字符会被转义,输出可能不是你想要的。选择模板包时先看目标格式。
小结
text/template 适合生成纯文本、Markdown、邮件正文和配置片段。它能把展示格式从业务代码里分离出来,让报表更容易调整。入门时掌握字段访问、range、if、函数注册、文件加载和错误处理,就能覆盖大部分场景。
模板不是业务逻辑的藏身处。复杂判断应该在 Go 代码里算好,模板负责展示。渲染前处理 parse 错误,渲染时写入 buffer,并为关键输出加测试。这样一份看似朴素的文本报表,也能写得稳定、清楚、可交接。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。