因为 gin.default() 对未匹配路由硬编码输出纯文本“404 page not found”,绕过模板引擎和中间件;必须用 noroute() 注册兜底路由并手动调用 c.html(404, "404.html", data) 才能渲染自定义页面。

为什么 gin.Default() 无法渲染自定义 404 页面
Gin 的 Default() 内置了 Recovery() 和 Logger() 中间件,但它对未匹配路由的处理是硬编码的:直接向 ResponseWriter 写入纯文本 "404 page not found",完全绕过模板引擎和任何中间件。这不是配置遗漏,而是设计如此——NoRoute() 之前,请求根本不会进入你写的 handler。
用 NoRoute() 注册兜底路由并手动调用 c.HTML()
必须显式注册一个全局兜底逻辑,且顺序不能错:
-
NoRoute()必须放在所有GET/POST路由注册之后,否则会提前拦截正常路径 - 模板文件需提前加载,例如
router.LoadHTMLGlob("templates/errors/*.html"),否则c.HTML()会 panic - 状态码必须显式传
404,否则默认返回200 - 所有动态数据(如当前路径、Referer)必须在
NoRoutehandler 里提取并塞进gin.H{},模板中无法访问c.Request等上下文对象
示例:
router := gin.Default()
router.LoadHTMLGlob("templates/errors/*.html")
<p>// 其他路由...
router.GET("/user", func(c <em>gin.Context) { /</em> ... */ })</p><p>// 必须放最后
router.NoRoute(func(c *gin.Context) {
c.HTML(404, "errors/404.html", gin.H{
"path": c.Request.URL.Path,
"referer": c.GetHeader("Referer"),
})
})
</p>
如何统一处理 500 等服务端错误
默认 gin.Recovery() 会返回 HTML 错误页,破坏 API 一致性。它和自定义错误中间件共存时还可能触发 http: multiple response.WriteHeader calls panic。
- 必须禁用默认 Recovery:
router.Use(gin.Logger()),但不要router.Use(gin.Recovery()) - 自己实现
CustomRecovery(),在defer中调用c.AbortWithStatusJSON(500, ...),并确保末尾有return阻止继续执行 - 业务层抛错应统一用自定义 error 类型(如
ErrInvalidParam),避免直接panic("xxx") - 不要在
CustomRecovery()里调用c.Error()—— 它只存错不输出,这里需要立即响应
模板里能用什么、不能用什么
Gin 模板渲染发生在 handler 执行完毕之后,此时 *gin.Context 已不可访问。所有变量都必须靠 gin.H{} 显式传入。
- ✅ 可用:
{{.path}}、{{.referer}}、{{.title}} - ❌ 不可用:
{{.Request.URL.Path}}、{{.Param "id"}}、{{.GetHeader "User-Agent"}}—— 这些会静默失败或 panic - ⚠️ 注意:如果模板用了
{{template "base" .}},确保base.html也在LoadHTMLFiles()或LoadHTMLGlob()范围内,否则加载失败
真正容易被忽略的是:NoRoute 是唯一可靠的 404 捕获点,而 CustomRecovery 是唯一可控的 panic 响应出口。两者缺一不可,且都必须手工介入,Gin 不会自动帮你桥接模板与错误流。











