gin.Default() 不会自动渲染 404.html,因其未匹配路由时直接返回纯文本,需显式注册 NoRoute() 并在所有路由后调用,配合 c.HTML(404, "404.html", data) 设置状态码与模板。

为什么 gin.Default() 不会自动渲染你写的 404.html
Gin 的 Default() 在路由未匹配时直接写入纯文本 "404 page not found",根本不会调用你注册的模板引擎。这不是漏配 LoadHTMLFiles(),而是压根没走到任何 handler —— NoRoute() 之前,模板连加载机会都没有。
必须用 NoRoute() 注册兜底路由,且顺序不能错
NoRoute() 是 Gin 唯一可靠的 404 捕获点,但它只对「前面所有路由都未匹配」的请求生效。常见翻车操作:
- 把
r.NoRoute(...)写在r.GET("/user", ...)之前 → 所有请求都被拦截,业务路由永不执行 - 在
r.Group()内部调用NoRoute()→ 不生效,它只属于整个Engine - 用
r.GET("/*path", handler)替代NoRoute()→ 语义不清、可能干扰正常通配逻辑
正确姿势:所有 r.GET、r.POST、r.Group 全部注册完,再调最后一句 r.NoRoute(...)。
c.HTML(404, "404.html", data) 必须显式传状态码
模板渲染不等于 HTTP 状态码设置。只写 c.HTML("404.html", data),响应头仍是 200;必须写成 c.HTML(404, "404.html", data)。
-
LoadHTMLFiles()或LoadHTMLGlob()必须提前加载对应模板,否则运行时 panic - 模板里只能用传入的 map key,比如
{{.path}};不能写{{.Request.URL.Path}},因为渲染时c已脱离 handler 上下文 - 想显示原始路径、Referer、User-Agent,得在
NoRoute函数里手动提取并塞进gin.H:"path": c.Request.URL.Path、"referer": c.GetHeader("Referer")
API 和页面路径要分开处理
同一个 NoRoute() 里,常需区分返回 JSON 还是 HTML。靠路径前缀判断最直接:
r.NoRoute(func(c *gin.Context) {
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api/") {
c.AbortWithStatusJSON(404, gin.H{"error": "endpoint not found"})
} else {
c.HTML(404, "404.html", gin.H{"title": "Page Not Found", "path": path})
}
})
注意大小写敏感:/API/ 和 /api/ 是两个不同路径;如果项目用 SPA,这里还可以 c.Redirect(http.StatusFound, "/") 跳首页。


















