Go 入门:用 text/template 生成一份朴素报表

从纯文本日报出发,介绍 text/template 的数据、循环、条件、函数、错误处理和测试方法。

不是所有输出都需要 HTML。很多内部工具会生成纯文本日报、邮件正文、Markdown 摘要、配置片段或工单说明。直接用 fmt.Fprintf 拼字符串可以起步,但字段一多,换行和缩进就会变得难维护。Go 标准库的 text/template 很适合这类场景。

text/templatehtml/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")

这样部署时不用担心忘记带模板文件。

错误处理

ParseExecute 都要处理错误。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、邮件正文和配置片段。它能把展示格式从业务代码里分离出来,让报表更容易调整。入门时掌握字段访问、rangeif、函数注册、文件加载和错误处理,就能覆盖大部分场景。

模板不是业务逻辑的藏身处。复杂判断应该在 Go 代码里算好,模板负责展示。渲染前处理 parse 错误,渲染时写入 buffer,并为关键输出加测试。这样一份看似朴素的文本报表,也能写得稳定、清楚、可交接。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页