NoRoute必须放在所有路由注册之后,因为它作为兜底机制仅捕获前面所有已注册路由均未匹配的请求;若提前注册,会拦截本应命中业务路由的请求,导致接口失效。

为什么 NoRoute 必须放在所有路由注册之后
因为 NoRoute 是 Gin 的兜底机制,只对「前面所有已注册路由都未匹配」的请求生效。如果写在 r.GET("/user", handler) 前面,那 /user 请求会被直接捕获,后续路由永远不会执行。
- 常见错误:把
NoRoute放在r.POST、r.Group之前,导致业务接口全部失效 - 正确顺序:先注册全部
r.GET、r.POST、r.Group,最后调用NoRoute - 即使用了
r.Group,也要确保整个分组注册完再挂NoRoute - 中间件不影响触发时机——它只看路由树是否匹配,不关心中间件是否 panic
如何让 NoRoute 返回 JSON 而不是默认纯文本
Gin 默认不给 NoRoute 做任何响应,如果你只写 c.JSON(404, ...) 不加终止操作,后续中间件(比如日志或 recover)可能继续执行,甚至重复写 header。
- 必须显式终止:用
c.AbortWithStatusJSON(404, ...),或c.JSON(404, ...); c.Abort() - 避免只调用
c.Status(404)而不写 body,客户端会收到空响应 - 生产环境建议复用统一的 error response 封装函数,比如
ErrorResponse.New(404, "not found") - 可带上下文数据:
"path": c.Request.URL.Path、"method": c.Request.Method
怎么用 NoRoute 渲染自定义 HTML 404 页面
Gin 默认的 404 是纯文本 "404 page not found",它绕过了模板引擎——因为路由未命中时根本不会进入任何 handler,模板自然没机会执行。
- 必须提前加载模板:
r.LoadHTMLFiles("templates/404.html")或r.LoadHTMLGlob("templates/**/*.html") - 在
NoRoute里手动调用c.HTML(404, "404.html", gin.H{...}),状态码要显式设为 404 - 模板中只能访问传入的 map 字段,比如
{{.path}},不能写{{.Request.URL.Path}} - 需要动态字段(如 Referer、User-Agent)得在 handler 里提取后塞进
gin.H,例如"referer": c.GetHeader("Referer")
如何区分 API 和页面路径做不同 404 处理
很多项目同时提供 REST API(如 /api/v1/users)和前端页面(如 /、/app/*),你可能希望 API 未命中返回 JSON,而页面路径未命中跳转到 SPA 入口或渲染 HTML。
- 靠字符串前缀判断:
strings.HasPrefix(c.Request.URL.Path, "/api/") - API 路径:用
c.AbortWithStatusJSON(404, ...) - 页面路径:用
c.Redirect(http.StatusFound, "/index.html")或c.HTML(404, "404.html", ...) - 注意大小写敏感:
/API/和/api/是不同路径 - 不要用正则匹配路径前缀,简单
strings.HasPrefix足够且高效
Abort() 调用——前者导致 panic,后者导致响应头冲突或内容被覆盖。NoRoute 看似简单,但顺序、终止、上下文传递这三点踩错一个就会出问题。


















