loadhtmlglob("templates/*/") 能递归加载所有模板文件,但因 gin 模板引擎不自动将路径映射为模板名,必须在每个 html 文件开头用 {{ define "admin/dashboard.html" }} 显式声明唯一模板名,否则同名文件会覆盖、渲染失败且无报错。

直接结论:用 LoadHTMLGlob("templates/**/*") 是最常用方式,但必须配合 {{ define "path/to/file.html" }} 显式声明模板名,否则多级目录下同名文件会冲突或加载失败。
为什么 LoadHTMLGlob("templates/**/*") 不能直接“开箱即用”
Gin 的模板引擎底层基于 Go 标准库 html/template,它不自动把文件路径映射为模板名。默认情况下,c.HTML(200, "index.html", ...) 中的 "index.html" 只认文件名(不含路径),所以如果 templates/admin/index.html 和 templates/user/index.html 同时被加载,后者会覆盖前者 —— 运行时不会报错,但渲染结果不可控。
-
LoadHTMLGlob("templates/*"):只加载templates/下一层的文件,子目录全忽略 -
LoadHTMLGlob("templates/**/*"):能递归匹配所有层级,但模板名仍需手动定义 -
LoadHTMLGlob("templates/**"):非法,会尝试加载目录本身,导致template: cannot parse "templates/admin": is a directory错误
必须在 HTML 模板里写 {{ define "xxx.html" }}
只要用了多级目录,就必须在每个模板文件开头显式 define 一个唯一标识符,这个标识符就是 c.HTML() 第二个参数的值。
- 模板
templates/admin/dashboard.html内容开头必须是:{{ define "admin/dashboard.html" }} - 模板
templates/public/home.html对应:{{ define "public/home.html" }} - 渲染时调用:
c.HTML(200, "admin/dashboard.html", data),不是"dashboard.html" - 不写
define或写错名称(比如漏掉.html后缀),c.HTML会静默失败并返回空响应或 500
替代方案:filepath.WalkDir + LoadHTMLFiles
当项目结构复杂、需要排除某些子目录(如 templates/_partials)、或想做运行时模板校验时,硬编码遍历更可控。
- 用
filepath.WalkDir("templates", ...)手动收集所有.html或.tmpl文件路径 - 过滤掉不需要的路径(例如跳过以
_开头的目录或文件) - 把路径切片传给
r.LoadHTMLFiles(tplFiles...)(注意是可变参数,不是切片) - 这种方式绕过了 glob 解析逻辑,路径完全由你控制,但每次增删模板都要重新编译
var tplFiles []string
filepath.WalkDir("templates", func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() && strings.HasSuffix(d.Name(), ".html") {
tplFiles = append(tplFiles, path)
}
return nil
})
r.LoadHTMLFiles(tplFiles...)
容易被忽略的路径和启动问题
模板路径是相对于**当前工作目录(cwd)**,不是 main.go 所在目录。用 go run main.go 时 cwd 是执行命令的目录;用 IDE(如 Goland)右键运行,默认 cwd 是项目根目录 —— 但某些配置可能指向 out/production 等构建目录,导致 templates/ 找不到。
- 调试时加一行:
fmt.Println("cwd:", os.Getwd())确认实际路径 - 避免使用相对路径如
./templates/**/*,直接用templates/**/* - 生产部署时,确保打包后的二进制文件与
templates/目录位置关系和开发时一致 - 模板中引用静态资源(CSS/JS)要用
gin.Static()显式挂载,跟模板加载无关但常一起出问题











