Gin 的 LoadHTMLGlob 并未真正缓存模板,每次 c.HTML() 仍需加锁执行,高并发下性能差;应启动时用 sync.Map 预编译并缓存所有子模板,handler 中直接调用 Execute 渲染,避免 Gin 内部开销。

模板每次请求都重新编译,性能必然垮掉——Gin 本身不缓存已编译的 template.Template,必须手动缓存。
为什么 LoadHTMLGlob 不等于“已缓存”
Gin 的 LoadHTMLGlob 和 LoadHTMLFiles 只在调用时解析并编译一次模板,但它们把结果存在 Gin 引擎内部的私有字段里,不对外暴露引用;后续每次 c.HTML() 都会从该字段中取出模板并执行,看似“只加载一次”,实则仍依赖 Gin 内部管理逻辑,且无法复用到自定义渲染流程(比如异步邮件模板、导出 PDF 等场景)。
更关键的是:这些方法不处理模板热更新,也不支持按需预热或并发安全访问。一旦你用 template.ParseFiles 手动构建模板,就彻底脱离 Gin 的模板管理,必须自己管生命周期。
- 现象:
LoadHTMLGlob("templates/**/*")启动快,但压测时 CPU 飙高,runtime/pprof显示大量text/template.(*Template).execute调用 - 原因:Gin 没有对
execute做额外缓存,它只是复用已编译对象,但并发高时sync.Mutex在内部模板执行路径上争抢严重 - 结论:真正要加速,得绕过 Gin 的模板绑定,直接操作
*template.Template并用sync.Map或fasttemplate类库做二级缓存
用 sync.Map 缓存编译后模板的正确姿势
不要在 handler 里调用 template.ParseFiles,也不要把 template.Template 存进普通 map——前者重复编译,后者并发 panic。
立即学习“go语言免费学习笔记(深入)”;
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
启动时一次性解析所有模板,存入 sync.Map,key 是模板名(如 "user/profile.html"),value 是 *template.Template:
var tmplCache sync.Map
func initTemplates() {
// 注意:ParseGlob 返回 *template.Template,不是 []template.Template
t, err := template.ParseGlob("templates/**/*.html")
if err != nil {
log.Fatal(err)
}
// 遍历所有子模板(包括 define 定义的命名模板)
for _, sub := range t.Templates() {
tmplCache.Store(sub.Name(), sub)
}
}
- 必须调用
t.Templates(),而非只存t主模板——否则{{template "header" .}}会报template: "header" not defined - 路径通配符用
**支持嵌套目录,但 Windows 下注意斜杠方向;Linux/macOS 无问题 - 若模板含
{{define}},确保每个define名称全局唯一,否则Store会覆盖
在 handler 中安全取模板并渲染
别再用 c.HTML(),它底层还是走 Gin 自己那套(不可控、无日志、难 debug)。直接从 sync.Map 取模板,调 Execute:
func render(c *gin.Context, name string, data interface{}) {
if tmpl, ok := tmplCache.Load(name); ok {
c.Header("Content-Type", "text/html; charset=utf-8")
if err := tmpl.(*template.Template).Execute(c.Writer, data); err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
return
}
c.AbortWithStatus(http.StatusNotFound)
}
- 必须显式设
Content-Type,否则浏览器可能乱码或当下载文件处理 -
tmplCache.Load()返回interface{},务必断言为*template.Template,否则运行时报 panic - 错误不能只打日志,要写回响应体,否则前端卡死无提示
- 避免在
Execute前做任何阻塞操作(如 DB 查询),否则缓存失去意义
静态资源路径与模板引用必须严格分离
缓存模板只解决服务端渲染速度,但 HTML 里写的 <link href="/css/app.css"> 仍是客户端发起的 HTTP 请求——这和模板缓存完全无关,必须靠 r.Static() 单独配置。
- 错误做法:
r.LoadHTMLFiles("templates/index.html")后以为 CSS 自动能加载,结果浏览器控制台全是404 /css/app.css - 正确配法:
r.Static("/css", "./static/css"),且确保 HTML 中路径前缀和注册的 URL 前缀一致 - 路径映射是纯字符串匹配,
r.Static("/static", "./assets")表示所有以/static/xxx开头的请求,都从./assets/xxx读文件 - 别用
StaticFS除非你要挂 zip 或 embed.FS——日常开发Static更轻量、调试更直观
最易被忽略的一点:模板缓存生效的前提,是你没在模板里写 {{.Now | nowFormat}} 这类需要 runtime 计算的函数——只要模板里有非纯数据引用,缓存就只是“少编译一次”,而不是“零开销”。真要极致优化,得把动态逻辑前置到 handler,模板只做纯展示。


















