应使用 pprof + 自定义 Handler 统计真实响应时间,关注 goroutine 泄漏、HTTP 客户端超时配置、中间件与路由开销,避免未设超时的阻塞调用和 channel 泄漏。

看 http.Server 的实际响应时间分布
Go 默认的 http.Server 不记录每条请求耗时,光靠日志里的 time.Now() 打点容易漏掉 TLS 握手、读取 body、写响应头等阶段。建议用 net/http/pprof + 自定义 Handler 组合观测:
- 启用
pprof:在服务启动时注册http.DefaultServeMux下的/debug/pprof/ - 加中间件统计真实
WriteHeader时间,而非仅defer time.Since()—— 因为WriteHeader可能被延迟调用(比如 streaming 场景) - 注意:
http.Server.ReadTimeout和WriteTimeout已被弃用,应改用ReadHeaderTimeout、IdleTimeout和WriteTimeout(Go 1.19+)
检查 Goroutine 泄漏与阻塞点
响应慢常因 goroutine 积压,而非 CPU 高。用 /debug/pprof/goroutine?debug=2 查看全量堆栈,重点关注:
- 大量处于
select或chan receive状态的 goroutine(说明 channel 未被消费或 sender 已死) - 卡在
database/sql.(*DB).QueryRow或http.(*Client).Do—— 很可能是连接池耗尽或下游无响应 - 使用
runtime.SetMutexProfileFraction(1)后访问/debug/pprof/mutex,确认是否有锁竞争热点
定位 HTTP 客户端调用瓶颈(含超时与重试)
Web 服务常依赖下游 API,但默认 http.Client 没设超时,会导致请求 hang 死:
-
http.Client.Timeout不控制 DNS 解析、TLS 握手、连接建立,需单独设Transport的DialContext和TLSHandshakeTimeout - 避免在 handler 内复用未配置
MaxIdleConnsPerHost的全局http.Client,否则高并发下会排队等待空闲连接 - 重试逻辑必须带指数退避 + jitter,且跳过非幂等方法(如
POST),否则可能引发重复提交
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 3 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
},
}验证中间件和路由匹配开销
用 chi、gin 等框架时,中间件链和路由树深度会影响首字节延迟:
立即学习“go语言免费学习笔记(深入)”;
- 禁用所有中间件,直连
http.HandlerFunc测 baseline 响应时间;再逐个开启对比 - 避免在中间件里做同步 I/O(如读文件、查 DB),尤其不要在
Logger中解析req.Body—— 会 consume body 导致后续 handler 读不到 - 路由通配符过多(如
/api/v1/*)会拖慢 trie 匹配,可改用更精确前缀(/api/v1/users/)并分离静态资源路径
真正卡住的地方,往往不是你怀疑的那行 db.QueryRow,而是前面某个没设超时的 http.Get,或者一个没人消费的 chan int。










