模板渲染前必须用c.Set()注入数据,且需在c.Next()前执行;键名小写加下划线、结构体字段需大写;仅c.HTML()可读取,404/静态文件路由虽经中间件但不渲染模板。

模板渲染前必须用 c.Set() 注入数据
Gin 的 HTML 模板无法自动访问中间件里的局部变量,必须显式调用 c.Set() 把值存进 *gin.Context,后续 c.HTML() 才能通过 .Get() 或模板函数取到。不调 c.Set() 就直接在模板里写 {{.UserInfo}},结果永远是空或 panic。
-
c.Set("site_name", "MyApp")是唯一可靠方式;ctx := c.Copy()或闭包捕获变量对模板无效 - 注入时机必须在
c.Next()之前(前置逻辑),否则 handler 可能已提前调用c.HTML() - 键名区分大小写,且不能含空格或特殊符号,推荐全小写加下划线:
c.Set("current_user_id", userID) - 如果注入结构体,确保字段首字母大写(Go 模板只能访问导出字段)
全局数据要避免在中间件里重复计算
比如用户信息、站点配置这类高频读取但低频变更的数据,别每次请求都查 DB 或调 API。中间件里应做缓存判断,或把加载逻辑下沉到初始化阶段。
- 错误做法:每次请求都在中间件里执行
user, _ := db.FindUserByID(c.GetInt("uid")) - 正确做法:启动时加载配置到全局变量,中间件只做
c.Set("config", globalConfig) - 若必须动态获取(如当前登录用户),优先用
c.Get("user")先查缓存,未命中再加载并c.Set() - 注意并发安全:不要在中间件里直接修改全局 map,除非加锁或用
sync.Map
c.HTML() 渲染时模板才能读到中间件注入的数据
只有调用 c.HTML()(或 c.Render())时,Gin 才会把 Context 中的 Keys 映射为模板的 .Data 字段。其他响应方式如 c.JSON()、c.String() 完全无视这些数据。
- 常见误用:
c.Set("title", "Home")后却调c.JSON(200, data)—— title 被完全丢弃 - 模板内取值写法固定:
{{.Data.site_name}}(Gin 默认把c.Keys放进.Data) - 想简化模板写法?可在中间件里统一构造 map 并
c.Set("tpl_data", tplMap),模板中用{{.tpl_data.title}} - 不建议覆盖
.Data本身,Gin 内部可能依赖它;自定义 key 更安全
404 和静态文件路由也会走全局中间件,但不触发模板渲染
这点极易被忽略:全局中间件对所有请求生效,包括未匹配路由(最终 404)和 r.Static() 提供的 JS/CSS/图片。但这些路径根本不会走到 c.HTML(),所以你注入的模板数据毫无意义,还白耗 CPU。
- 验证方式:在中间件开头加
log.Printf("path=%s, handler=%s", c.Request.URL.Path, c.HandlerName()) - 发现
handler是github.com/gin-gonic/gin.(*Engine).handle404或github.com/gin-gonic/gin.(*Engine).serveFile,就该跳过c.Set() - 轻量判断:用
strings.HasPrefix(c.Request.URL.Path, "/static/")或c.HandlerName() == "github.com/gin-gonic/gin.(*Engine).handle404" - 健康检查接口(如
/health)也建议绕过模板数据注入,纯 JSON 响应更高效
中间件注入模板数据这件事,表面只是几行 c.Set(),实际陷阱全在执行时机、作用域和副作用上——尤其是 404 和静态资源那条隐性通路,不打日志根本意识不到它也在默默执行你的“全局”逻辑。











