必须在应用启动时一次性预加载并解析全部模板到单个template.template实例中,使用template.must(template.parseglob("templates//.html"))确保语法正确且错误前置暴露;所有嵌套模板须通过{{define}}和{{template "name" .}}组织,透传上下文点号(.)以保障字段可访问,禁用模板内业务计算与非内置函数调用,并严格遵循结构体字段导出规则及content-type设置。

用 html/template 做服务端渲染,不是“能不能用”,而是“必须这么用”——text/template 渲染 HTML 就是 XSS 漏洞上线。
模板解析必须在启动时完成,别放 handler 里
每次 HTTP 请求都调用 template.ParseFiles 或 template.ParseGlob,等于让 CPU 重复做词法分析和 AST 构建。小流量看不出,QPS 上百就明显卡顿。
- 正确做法:应用启动时一次性加载全部模板,存为全局变量,例如
var tpl *template.Template - 推荐写法:
template.Must(template.ParseGlob("templates/**/*.html")),错误在启动阶段暴露,不等到用户访问才 panic - 所有模板(
base.html、header.html、post/list.html)必须进同一个*template.Template实例,否则{{template "header"}}找不到目标
嵌套模板传参必须带点:{{template "header" .}}
{{template "header"}} 看似省事,实际传的是空上下文,子模板里所有 {{.Title}} 都为空。这不是 bug,是作用域设计。
- 那个
.表示当前执行上下文的完整数据,透传进去才能在子模板中正常访问字段 - 如果子模板需要额外信息(比如当前页面类型),得提前组织进结构体字段,而不是试图传多个参数:
{{template "header" .}}是唯一合法形式 -
{{define "header"}}块内直接用{{.Title}},不用{{$}}或{{$.Title}}(除非你在{{range}}内想跳回顶层)
模板里禁止业务计算,数据必须预处理
{{range .Posts}}{{len .Comments}}{{end}} 这类写法,在 100 条记录以上就会拖慢渲染速度——每次循环都触发反射取长度、接口转换、map 查找。
- 日期格式化、状态码转中文、权限判断等,全在 Go 层做完,传入结构体字段已是最终字符串
-
len、index、print等是内置函数,但strings.Contains、time.Now不是,不能直接在模板里调用 - 深层嵌套
{{range}}(比如{{range .Users}}{{range .Posts}})要警惕,容易掩盖性能问题
路径和 Content-Type 是两个高频翻车点
template.ParseFiles("templates/index.html") 报错 “no such file”,往往不是文件不存在,而是 Go 按 os.Executable() 目录找路径,不是你 go run 的目录。
- 开发期可临时用
execDir, _ := filepath.Abs(filepath.Dir(os.Executable()))拼路径;生产环境更推荐embed.FS(Go 1.16+),编译时打包,彻底规避路径问题 -
t.Execute(w, data)前漏掉w.Header().Set("Content-Type", "text/html; charset=utf-8"),轻则浏览器当纯文本显示,重则含中文时 panic - 传给
Execute的data不能是nil指针,空结构体可以,nil会 panic
最易被忽略的其实是字段导出规则:结构体字段必须首字母大写(Name string),小写(name string)在模板里完全不可见;还有 html/template 对 onclick=、href="javascript:" 的上下文感知转义,是 text/template 根本不具备的安全能力——这点一旦选错,连测试环境都算不上安全。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











