直接回答:泄露即 goroutine 永远阻塞无法退出。典型模式:向无接收者的 channel 发送(缓冲满或无缓冲)、等待永不关闭的 channel、select 缺 default 且所有 case 都不就绪、http 响应 Body 未 Close(连接 goroutine 挂着)、time.After 在循环里(1.23 前定时器不回收)、锁获取顺序错误死锁。检测:runtime.NumGoroutine 持续增长 + goroutine profile 看栈。
展开解析:定位流程:/debug/pprof/goroutine?debug=2 导出全量栈,按栈顶函数分组(pprof 的 traces 视图自动聚合),成百上千个 goroutine 卡在同一行即实锤;找泄露源头看栈里“谁创建了它”——goroutine 栈底是创建入口。防护模式按模式给:channel 通信必配退出信号——context.Context 贯穿,select 里永远有 ctx.Done() 分支;写 channel 前想清楚接收方生命周期,生产者不知消费者死活就用带缓冲加 select default 丢弃策略;http 客户端必须 io.Copy(io.Discard, resp.Body) 加 Close(连接复用的前提);定时任务用 time.Ticker 并 Stop,或 1.23+ 信任新 GC 回收无引用定时器;worker pool 关闭用 close(jobChan) 通知退出加 WaitGroup 等待。工程防线:单测用 goleak(go.uber.org/goleak)断言测试结束无残留 goroutine,CI 拦泄露;线上把 NumGoroutine 挂监控,斜率异常即告警;日志里周期性打 goroutine 数辅助事后归因。记住泄露的代价不止内存:每个 goroutine 起始 8KB 栈,泄露的 goroutine 还持有它引用的所有对象。
追问方向:为什么 http.Body 不 Close 会泄露 goroutine?select+ctx 模式的遗漏点有哪些?
(约 490 字)