应统一用 http.handler 包裹所有请求入口以实现 panic 恢复和 context 超时控制:recover 必须在 servehttp 内 defer 执行,超时需从 r.context() 派生子 context 并传入下游调用。

用 http.Handler 包裹所有 HTTP 请求入口
Go 的 http.ServeMux 本身不提供中间件能力,但 http.Handler 接口天然支持链式包装。统一拦截的起点不是改路由注册方式,而是把每个 handler 都套一层异常捕获逻辑。
常见错误现象:直接在 handler 函数里写 recover() 没用——panic 发生在子 goroutine(比如 http.HandlerFunc 内部调用的异步函数)时,主协程早已退出。
- 必须在
http.ServeHTTP方法内做defer/recover,且要包裹整个 handler 调用过程 - 不要用
http.ListenAndServe默认 mux,自己实现一个带 recover 的http.Handler - 如果用了
gorilla/mux或chi,它们的Mux本身实现了http.Handler,可直接包装,无需改路由结构
func recoverHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
log.Printf("PANIC: %v", err)
}
}()
next.ServeHTTP(w, r)
})
}
网络超时和连接中断要靠 context.Context 控制
HTTP 层面的 timeout(如 http.Server.ReadTimeout)只管连接建立和首行读取,对 handler 内部耗时、下游 HTTP 调用、数据库查询等完全无效。真正能统一拦截“慢请求”和“下游无响应”的只有 context.Context。
使用场景:你写了 http.Post 调第三方 API,对方卡住 30 秒,你的服务就卡住 30 秒,还占着 goroutine —— 这类问题不能靠外围 recover 解决。
- 所有 handler 入口应从
r.Context()派生带超时的子 context,例如ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - 下游调用(
http.Client.Do、database/sql.QueryContext、redis.Client.Get)必须显式传入该 context - 别依赖
time.Sleep模拟耗时——它不响应 cancel;改用select { case
net/http 错误类型得分类处理,不能全当 500
Go 标准库里很多网络错误是 *url.Error 或 *net.OpError,它们嵌套了底层原因(如 connection refused、i/o timeout、no route to host),但都实现了 error 接口,直接打印或返回会丢失关键信息。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
容易踩的坑:用 strings.Contains(err.Error(), "timeout") 判断超时——不稳定,不同系统/Go 版本错误消息格式可能变。
- 用
errors.As提取具体错误类型:var urlErr *url.Error; if errors.As(err, &urlErr) { ... } - 对
urlErr.Err再次用errors.Is判断是否为超时:errors.Is(urlErr.Err, context.DeadlineExceeded) - 区分客户端错误(4xx)和服务器错误(5xx):DNS 失败、连接拒绝属于服务端可重试问题,应返回 503;而
401 Unauthorized来自下游,应透传而非吞掉
模块化封装时注意 http.ResponseWriter 的不可逆性
想统一记录响应状态码、耗时、body 大小?别直接包装 http.ResponseWriter 后忘了它是一次性的——一旦调用过 WriteHeader 或 Write,再尝试修改 header 就会 panic(http: multiple response.WriteHeader calls)。
性能影响:每次请求都 new 一个 wrapper 结构体开销极小,但若在里面做 JSON 序列化或日志写磁盘,会拖慢 fast path。
- 用
struct{ http.ResponseWriter }匿名嵌入,重写WriteHeader和Write方法来捕获状态码和字节数 - 别在 wrapper 里读取原始 body(
r.Body)——它只能读一次,后续 handler 就拿不到数据了;如需解析,用io.TeeReader或提前ioutil.ReadAll并重设r.Body - 日志和监控上报尽量异步(
go func(){...}()),避免阻塞响应流
最复杂也最容易被忽略的一点:context 取消、panic、handler 显式 return、以及 http.CloseNotify(已废弃但老代码可能还在用)这四类退出路径,都要确保资源清理和日志落盘 —— 尤其是连接池里的 http.Client,漏关会导致 fd 耗尽。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










