gin 本身不支持原生模板继承,需通过 html/template 的 {{define}} 和 {{template}} 机制实现;关键在于所有含 define 的模板必须一次性加载到同一 template 实例,否则调用时会报 “undefined” 错误。

Gin 本身不提供模板继承语法(比如 Django 的 {% extends %}),但能通过 Go 标准库 html/template 的 {{ template }} 和 {{ define }} 机制实现公共布局嵌套——关键在于「主模板里定义区块,子模板里执行定义并调用」,而不是反过来。
为什么 c.HTML() 渲染时找不到 {{ template "main" }}
常见错误是:在 base.html 里写了 {{ define "main" }},又在 index.html 里写 {{ template "main" . }},但渲染时提示 template: "main" is not defined。这是因为:
- Gin 加载模板时只解析文件内容,不自动合并或识别“父子关系”
-
{{ template "xxx" }}引用的模板名必须已由{{ define "xxx" }}在**已加载的任一模板文件中声明过** - 如果
base.html和index.html是分开加载的,且base.html没被显式执行(比如没被{{ template "base" }}调用),它的define就不会注册进模板集合
解决办法:把所有参与嵌套的模板一次性加载,并确保 base.html 是第一个被解析的(它定义区块),再让子模板只做 {{ template "main" . }} 调用。
LoadHTMLGlob() 必须包含 base 模板且路径顺序无关
LoadHTMLGlob("templates/**/*") 会递归加载匹配的所有文件,但 Go 的 template.ParseGlob 内部会按文件名排序后依次解析——所以只要 base.html 文件名排在前面(如命名为 _base.html 或 00_base.html),它的 {{ define }} 就会被先注册。
更稳妥的做法是显式控制加载顺序:
- 用
LoadHTMLFiles("templates/base.html", "templates/index.html", "templates/user.html")明确列出 base 在前 - 或统一用
LoadHTMLGlob("templates/*.html")并命名 base 为_base.html(下划线开头通常排最前) - 避免在子模板里重复
{{ define "base" }}——那会导致模板名冲突
典型嵌套结构怎么组织
一个可运行的最小结构:
templates/_base.html:
{{ define "base" }}
<title>{{ .title }}</title><header>公共头部</header><main>{{ template "content" . }}</main><footer>公共底部</footer>
{{ end }}
templates/index.html:
{{ template "base" . }}
{{ define "content" }}
<h1>首页内容</h1>
<p>标题:{{ .title }}</p>
{{ end }}
注意两点:
-
index.html开头就调用{{ template "base" . }},触发 base 定义的渲染 -
content区块定义在index.html里,但它是在base.html的{{ template "content" . }}位置被插入的——这是 Go 模板的“延迟执行”特性 - 渲染时调用
c.HTML(http.StatusOK, "index.html", gin.H{"title": "首页"}),Gin 会找到index.html并执行其中的template "base",进而展开整个结构
容易忽略的坑:静态资源路径和子模板变量作用域
嵌套后常出现 CSS 不加载、.title 在 base.html 里取不到值等问题,原因有:
-
base.html里的{{ .title }}能访问到数据,是因为传入的是同一个.(即gin.H{...}),但如果你在base.html里用了{{ with .user }}这类作用域变更,子模板的{{ .title }}就会失效——因为with改变了当前点(.) - 静态资源路径(如
<link href="/static/css/app.css">)要和r.Static("/static", "./static")的注册路径严格一致;嵌套不改变 HTML 输出路径,所以路径逻辑和单文件模板完全一样 - 不要在
base.html里用{{ range .items }}然后期望子模板能继续用.访问原始数据——range会把.变成当前 item,应在range外保留{{$root := .}}并在需要时用{{$root.title}}
嵌套本身不复杂,难的是理解 Go 模板的执行时作用域和定义/调用分离机制——一旦搞清 define 是注册、template 是调用,且所有模板共享同一份数据对象,大部分问题就迎刃而解。











