Gin 中无原生 slot,但可用 {{define}} + {{template}} 或 {{block}} 模拟:前者实现命名模板块注入,后者支持默认内容+覆盖;需确保模板加载、名称一致、数据可导出,并注意服务端编译期替换与 XSS 防护。

Go 标准库的 html/template 本身没有“插槽(slot)”这个概念,Vue 或 React 那种语义化的 在 Gin 模板里不存在。但你可以用 {{define}} + {{template}} 组合模拟出等效行为——本质是命名模板块 + 动态注入,不是运行时 DOM 插槽,而是服务端模板编译期的文本替换。
用 {{define}} 和 {{template}} 实现“插槽”语义
所谓“插槽”,在 Gin 场景下就是:主模板预留占位区域(比如 {{template "content" .}}),子模板或数据上下文决定该位置渲染什么。这依赖两个动作:定义可复用模板块、按名调用并传参。
-
{{define "content"}}块必须出现在被ParseFiles或LoadHTMLGlob加载的任意模板文件中(可以是同一个文件,也可以是单独的partials/content.tmpl) -
{{template "content" .}}中的.是当前作用域数据,会透传进被调用模板;若需限定数据,可写成{{template "content" $someData}} - 多个同名
{{define}}会覆盖——后加载的生效,所以注意文件加载顺序(LoadHTMLGlob("templates/**/*")按字典序读取)
{{block}} 比 {{template}} 更适合“默认内容 + 覆盖”场景
如果你希望主模板提供默认 HTML,同时允许子模板覆盖,{{block}} 是更直接的选择。它等价于 “先尝试 {{template}},找不到就渲染内部内容”。
- 写法:
{{block "sidebar" .}}默认侧边栏{{end}} - 子模板中只需
{{define "sidebar"}}即可替换,默认内容自动丢弃自定义侧边栏
{{end}} - 注意:
{{block}}必须和{{define}}在同一套模板解析上下文中,不能跨template.ParseFiles独立调用
常见错误:为什么 {{template "xxx"}} 渲染为空?
这是最常卡住的地方,根本原因几乎总是模板未被加载或名称不匹配。
- 检查是否调用了
r.LoadHTMLGlob("templates/**/*")或r.LoadHTMLFiles(...),且路径包含定义了{{define "xxx"}}的文件 - 确认
{{define "xxx"}}和{{template "xxx"}}中的字符串完全一致(区分大小写、空格、引号) - 如果用
gin.H{}传参,确保结构体字段首字母大写(如Name),否则模板内{{.Name}}取不到值 - 不要在
{{template}}外部加多余换行或空格——html/template默认保留空白,可能破坏 HTML 结构
避免 XSS 时的插槽内容处理
当“插槽”内容来自用户输入(比如 CMS 后台富文本),直接 {{template "content" .}} 不会自动转义——因为 {{template}} 是原样插入已编译的模板片段,而转义只发生在 {{.RawHTML}} 这类变量输出时。
- 安全做法:把用户内容封装为
template.HTML类型再传入,例如c.HTML(200, "page.tmpl", gin.H{"Content": template.HTML(userInput)}) - 若想让插槽内容也走默认转义,就别用
{{template}},改用变量输出:{{.Content}},并在后端做template.HTMLEscapeString()处理 - 切勿在模板里写
{{.Content | safe}}并传入未过滤的 HTML——这等于主动绕过防护
真正要注意的不是“怎么写插槽”,而是理解 html/template 的执行时机:所有 {{define}} 都在服务启动时解析完成,没有运行时动态注册;所谓“替换”,只是编译好的模板块按名拼接,不是客户端 JS 那种响应式更新。这点一旦混淆,后续调试会反复踩坑。











