必须换用text/template而非html/template,手动创建实例并调用r.SetHTMLTemplate()注入,模板中用{{-}}控制空白,扩展名建议.txt且不写HTML标签。

模板文件里怎么写纯文本内容,不被 HTML 转义?
Gin 默认用 html/template,它会自动转义所有输出(比如 → <code>),这对 HTML 安全很重要,但对纯文本邮件是灾难——你会收到一堆乱码符号,而不是换行、缩进、冒号分隔的可读内容。
关键不是“禁用转义”,而是**换用 text/template**。Gin 本身不强制绑定 html/template,只要你在 SetHTMLTemplate() 时传入 text/template 实例,就能原样输出。
- 别用
r.LoadHTMLGlob("templates/*.txt")—— 这个方法内部硬编码用了html/template,即使后缀是.txt也会转义 - 必须手动创建
text/template实例,并用r.SetHTMLTemplate()注入(没错,函数名带 “HTML” 但实际接受任意*template.Template) - 模板文件扩展名建议用
.txt或.tmpl,但内容里**不能写任何 HTML 标签**,只用 Go 模板语法:{{.Name}}、{{range .Items}}、{{. | printf "%.2f"}} 等
如何在 Gin 中加载并渲染 text/template?
直接替换默认模板引擎,绕过 LoadHTMLGlob 的限制:
import (
"text/template"
"github.com/gin-gonic/gin"
)
<p>func main() {
r := gin.Default()</p><pre class="brush:php;toolbar:false;">// 手动加载纯文本模板
t := template.Must(template.New("").ParseFiles("templates/email.txt"))
r.SetHTMLTemplate(t) // 注意:这里用的是 SetHTMLTemplate,但传 text/template 也合法
r.GET("/send", func(c *gin.Context) {
data := gin.H{
"To": "user@example.com",
"Subject": "订单确认",
"OrderID": "ORD-2026-7890",
"Items": []map[string]interface{}{
{"Name": "咖啡机", "Price": 499.00},
{"Name": "滤纸", "Price": 12.50},
},
}
c.HTML(200, "email.txt", data) // 渲染时仍用 c.HTML,但底层是 text/template
})
r.Run()}
注意:c.HTML() 这个方法名有误导性,它只是 Gin 统一的模板渲染入口,实际行为取决于你注入的模板引擎类型。
邮件模板里怎么控制换行和空格?
text/template 默认会保留所有空白符,但 Go 模板的 {{}} 语法自带换行污染(比如 {{range}} 块前后多出空行)。容易让邮件格式错乱。
- 用
{{- }}和{{-}}去掉左/右空白:{{- .Name -}} 不会在前后加换行或空格 - 模板开头加
{{" "}}(一个空格)或{{"\n"}}显式控制首行缩进/空行 - 避免在模板里写多余空行——
text/template不像 HTML 那样忽略空白,它就是原始文本 - 测试时用
curl http://localhost:8080/send查看响应体,确认换行符是\n而非\r\n(Gin 默认输出 Unix 换行,符合 SMTP 规范)
为什么不用 c.String() 拼接,而要用模板?
硬拼字符串看似简单,但很快会失控:
- 嵌套结构(如订单含多个商品)需要手写循环 + 字符串累加,易漏边界、难维护
- 日期格式化、金额千分位、状态映射("paid" → "已支付")等逻辑混在 handler 里,违反关注点分离
- 无法复用:同一份邮件内容可能用于 Web 预览、API 返回、真实发送,模板一次定义,多处调用
- 测试困难:拼接逻辑散落在代码各处,没法单独单元测试模板渲染结果
真正麻烦的不是“怎么让模板输出纯文本”,而是**别让模板依赖 HTML 渲染路径、别忽略空白控制、别误信函数名含义**——这些细节卡住的人,比语法不会写的还多。


















