Go的html/template不支持递归,需用{{define "name"}}定义命名模板并显式传参调用,且必须顶层定义、统一字段名、避免循环引用、控制层级深度、确保模板加载正确。

Go 的 html/template 本身不支持递归,但可以安全模拟
直接写 {{template "menu-item" .}} 在自身模板里会 panic,因为 Go 模板引擎禁止无限递归调用。必须用 {{define "menu-item"}} 显式定义一个命名模板,再通过 {{template "menu-item" .Children}} 显式传参调用——这不是语法糖,而是唯一能控制终止条件的安全方式。
常见错误现象:模板里漏掉 {{if .Children}} 判断,导致空切片仍触发一次 {{template}} 调用,渲染出多余 <ul></ul> 或 panic;或者把 {{define}} 写在 {{range}} 里面,结果只在当前作用域生效,递归时找不到模板。
- 必须将
{{define "menu-item"}}放在顶层(不能嵌套在{{range}}或{{with}}中) - 每次递归都需显式传入子节点,如
{{template "menu-item" .Children}},不能依赖当前. - 子节点字段名要统一,比如后端 struct 定义为
Children []MenuItem,模板里就固定用.Children
后端数据结构必须提前扁平化或严格避免循环引用
如果菜单数据是嵌套 struct,比如 type MenuItem struct { Name string; Children []MenuItem; Parent *MenuItem },模板渲染时可能因 Parent 字段形成循环引用,在 json.Marshal 或深度遍历时直接 panic。
推荐做法是:在 Gin handler 中就完成树形构建,传给模板的是纯值类型切片,例如 []MenuItem,而不是 []*MenuItem —— 后者一旦某个 Children 元素为 nil,模板执行就会失败。
- 用父 ID 扁平化数据 + 服务端递归组装,比前端 JS 处理更可控
- 层级过深(>6 层)时,模板递归容易触发 goroutine stack overflow,建议后端只传前 3–4 层,深层靠前端展开逻辑
- URL、图标、是否激活等展示字段,务必在 handler 中补全,不要在模板里用
if做业务判断
{{template}} 调用时容器标签必须包裹在条件判断内
单纯用 {{range .Children}} 渲染子菜单很危险:当 .Children 是空切片时,{{range}} 内部完全不执行,但你通常仍需要一个空的 <ul></ul> 容器来维持 CSS 样式或 JS 行为。
正确写法是先判断是否存在子项,再决定是否渲染容器和内容:
{{if .Children}}
<ul class="submenu">
{{range .Children}}
<li>{{.Name}}</li>
{{end}}
</ul>
{{end}}
但更推荐把整个子菜单逻辑封装进 {{define "menu-item"}},并在其中统一处理容器与递归调用:
{{define "menu-item"}}
<li>
<a href="{{.URL}}">{{.Name}}</a>
{{if .Children}}
<ul>
{{range .Children}}
{{template "menu-item" .}}
{{end}}
</ul>
{{end}}
</li>
{{end}}
Gin 加载模板时路径和命名必须匹配 glob 规则
如果模板文件放在 templates/layout/menu.tmpl,而你用 r.LoadHTMLGlob("templates/*.tmpl"),那这个文件根本不会被加载,{{template "menu-item"}} 会报错 template: menu-item is undefined。
实际中容易忽略两点:一是 LoadHTMLGlob 的 glob 模式必须覆盖所有模板文件路径;二是局部模板名({{define "xxx"}})和 {{template "xxx"}} 中的字符串必须完全一致,大小写敏感,且不能带文件扩展名。
- 推荐统一用
r.LoadHTMLGlob("templates/**/*"),确保子目录下所有.tmpl或.html都被加载 - 局部模板名建议用语义化短名,如
"menu-item"、"nav-link",避免带路径或版本号 - 调试时可加
log.Println("loaded templates:", template.Templates())查看已注册模板列表


















