go标准库http.servemux不自动处理404/500页面,需手动注册兜底路由或包装handler拦截状态码;推荐用responsewriter包装器捕获writeheader调用并统一注入自定义html错误页。

HTTP 错误码对应 handler 需手动注册
Go 标准库的 http.ServeMux 不自动处理 404、500 等错误页面,它只负责路由匹配成功时的 handler;匹配失败时直接调用 http.NotFound,而这个函数硬编码返回纯文本 "404 page not found"。想换 HTML 页面,必须自己接管错误响应逻辑。
常见做法是包装 http.Handler,在 ServeHTTP 中捕获 panic 或检查响应状态码,但更可控的方式是:用自定义 mux(比如 http.ServeMux 的子类)或中间件式 wrapper,在写响应前拦截非 2xx 状态。
- 不要依赖
http.Error自动渲染——它只写状态行和简单文本,不支持模板 - 避免在每个 handler 里重复写
if err != nil { renderError(w, 500, err) },容易漏掉或状态码错配 - 推荐统一入口拦截:在顶层 wrapper 中判断
w.Header().Get("Content-Type") == ""且状态码非 2xx 时重写响应
用 http.ResponseWriter 包装器捕获状态码
标准 http.ResponseWriter 接口不暴露已写入的状态码,所以得用包装器记录。定义一个结构体,嵌入原始 ResponseWriter,重写 WriteHeader 方法来捕获状态码,并标记是否已写头:
type statusWriter struct {
http.ResponseWriter
statusCode int
written bool
}
func (w *statusWriter) WriteHeader(code int) {
w.statusCode = code
w.written = true
w.ResponseWriter.WriteHeader(code)
}
func (w *statusWriter) Write(b []byte) (int, error) {
if !w.written {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}
这样就能在 wrapper 中判断 sw.statusCode 是否为 404/500 并插入自定义 HTML:
- 务必在
Write前调用WriteHeader,否则 Go 会默认写 200,导致后续无法修改状态码 - 包装器要透传
Header()、Flush()等方法,否则影响 gzip、streaming 等行为 - 不要在包装器里直接调用
Write渲染错误页——应留到 handler 返回后、实际写响应前统一处理,避免重复写
静态错误页与动态模板的选择
如果错误页不含动态内容(如时间、请求 ID),直接用 http.ServeFile 或预读取的 []byte 最快;若需注入错误信息(如 500 时显示简短错误原因),用 html/template 更安全。
示例:用模板渲染 500 页面,带错误摘要:
var errorTmpl = template.Must(template.New("error").Parse(`
<h1>{{.Code}}</h1>
<p>{{.Message}}</p>
`))
func renderError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
errorTmpl.Execute(w, map[string]interface{}{
"Code": status,
"Message": msg,
})
}
- 模板文件路径不要硬编码,用
embed.FS(Go 1.16+)打包进二进制,避免部署时缺文件 - 生产环境禁用模板中的
{{.Err}}全量输出——可能泄露敏感路径或堆栈,只取err.Error()的前 100 字符 - 404 页面建议保留简洁导航(如首页链接),但不要自动跳转,干扰调试
中间件顺序与 panic 恢复的边界
自定义错误页中间件必须放在链最外层,否则内层 panic 可能被上层中间件吞掉,导致状态码仍是 200。同时,recover() 只捕获当前 goroutine 的 panic,HTTP handler 是独立 goroutine,所以恢复逻辑必须在 handler 内部或紧邻 wrapper 中做。
典型错误顺序:
- ❌ 把错误页 wrapper 放在日志中间件之后 → 日志已记下 200,但实际返回了 500 HTML
- ❌ 在 main 函数里 defer recover() → 完全无效,panic 发生在 handler goroutine
- ✅ 正确做法:用 wrapper 包裹 handler,在
defer func()中 recover,然后设置sw.statusCode = 500并触发渲染
真正难处理的是第三方库引发的 panic(比如模板执行时 nil pointer),它们不会经过你的 handler 逻辑。这种场景下,wrapper 的 recover 是唯一兜底手段,但要注意:recover 后不能继续写响应体,必须立即渲染错误页并 return。











