LoadHTMLGlob 什么时候解析模板?debug 与 release 差多少?放在子目录里的同名模板会怎样?出错的时候用户看到什么?这篇用真实的模板文件把 Gin 的 HTML 渲染一步步试出来。
gin.go 的 LoadHTMLGlob(Gin v1.12.0):
272 | func (engine *Engine) LoadHTMLGlob(pattern string) {
273 | left := engine.delims.Left
274 | right := engine.delims.Right
275 | templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
276 |
277 | if IsDebugging() {
278 | debugPrintLoadTemplate(templ)
279 | engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
280 | return
281 | }
282 |
283 | engine.SetHTMLTemplate(templ)
284 | }
render/html.go: 54 | func (r HTMLProduction) Instance(name string, data any) Render {
render/html.go: 63 | func (r HTMLDebug) Instance(name string, data any) Render {
render/html.go: 71 | func (r HTMLDebug) loadTemplate() *template.Template {
render/html.go: 76 | return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...))
render/html.go: 79 | return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
render/html.go: 82 | return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFS(debug 模式用 HTMLDebug:每次渲染都重新 ParseGlob,所以改模板立刻生效,开发很方便;release 模式用 HTMLProduction,启动时解析一次。这也是线上必须设置 GIN_MODE=release 的原因之一:debug 模式每次渲染多出上百微秒的解析开销,还会有文件 I/O。
/i → 200 Content-Type="text/html; charset=utf-8"
<h1>标题</h1><p>正文 <b>粗体</b> & 'q' <script>alert(1)</script></p>
/u → 200 Content-Type="text/html; charset=utf-8"
<div class="u">ANN - <em>可信的 HTML</em></div>
/u2 → 200 Content-Type="text/html; charset=utf-8"
<div class="u">BOB - <em>不可信</em></div>
/p → 200 Content-Type="text/html; charset=utf-8"
<html><body><main>嵌套模板</main></body></html>用的是标准库的 html/template,会按上下文自动转义(HTML 文本、属性、JS、URL 各不相同)。用户输入里的 <script> 会变成实体。只有明确标成 template.HTML 的值才原样输出,所以不要把用户输入转成这个类型。
GIN_MODE=release 改模板文件前 "<h1>T</h1><p>B</p>" → 改之后(不重启)"<h1>T</h1><p>B</p>" GIN_MODE=release 渲染一次 index.html(glob 匹配 5 个模板文件):中位 1.7 µs GIN_MODE=debug 改模板文件前 "<h1>T</h1><p>B</p>" → 改之后(不重启)"<h1>改过的模板 T</h1>" GIN_MODE=debug 渲染一次 index.html(glob 匹配 5 个模板文件):中位 119.5 µs
ParseGlob 用文件的 base name 做模板名,不含目录。tpl/*/*.html 下两个目录里的 item.html 会冲突,后一个静默覆盖前一个。要避免,在文件里用 {{define "a/item.html"}} 起带目录的唯一名字,再按这个名字渲染。
/a → <i>B 目录的 item</i>
/u → <i>带目录前缀的名字</i>
/uu →模板名不存在、渲染中途出错,Gin 都只是 c.Error 并 Abort:前者客户端看到 200 加空页面,后者看到截断的页面(模板边执行边写出)。这些错误只在 c.Errors 里,要靠错误处理中间件或日志发现。想避免半截页面,可以自己先 ExecuteTemplate 到缓冲区,成功后再写出。
/nope → 状态 200 正文 ""
/partial → 状态 200 正文 "ok"
c.Errors: html/template: "nope.html" is undefined
c.Errors: template: partial.html:1:13: executing "partial.html" at <.Missing.Field>: can't evaluate field Missing in type struct { X int }
先 LoadHTMLGlob 后 SetFuncMap(模板里用了 upper)→ panic: template: user.html:1: function "upper" not definedSetFuncMap 必须在 LoadHTMLGlob 之前调用,否则模板里引用的函数在解析时还不存在,LoadHTMLGlob 直接 panic(这是启动期的错误,不会漏到线上)。