最直接证据是 runtime.NumGoroutine() 持续上涨且业务稳定后不回落,配合 /debug/pprof/goroutine?debug=2 查看堆栈,重点观察大量 goroutine 卡在 chan receive、select、semacquire 或 IO wait 状态。

怎么确认 goroutine 真的泄漏了?
不是数量多就叫泄漏,而是该退出的没退出、该结束的卡住了。最直接证据是 runtime.NumGoroutine() 持续上涨,且在业务稳定后不回落。
用 pprof 查看实时堆栈:启动时加 http.ListenAndServe("localhost:6060", nil),然后访问 http://localhost:6060/debug/pprof/goroutine?debug=2。重点看那些停在 、<code>select、time.Sleep 或 net.Conn.Read 的 goroutine —— 它们大概率没出口。
- 别只看总数,对比「空载」和「压测后」的差值;差值长期不回收才可疑
- 如果某类 goroutine 在请求结束后仍存在(比如每个 HTTP 请求都启一个但没随 ctx 结束),基本就是泄漏
- 第三方库(如某些数据库驱动、日志 agent)可能悄悄起 goroutine,务必查其文档是否需显式
Close()
为什么加了 context 还泄漏?
context 只是信号源,不自动终止 goroutine。常见错误是:写了 ctx.Done() 但没在关键阻塞点检查,或者把 select 写错位置。
典型反例:for range ch 之前没判断 ctx.Done(),结果 channel 没关、ctx 已取消,goroutine 卡在 range 等永远不来的新数据。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 所有阻塞操作都要进
select:包括、<code>ch 、<code>time.Sleep、http.Get - 不要在 goroutine 入口只 check 一次
ctx.Err(),要在每次循环/每次 IO 前检查 - 用
context.WithCancel而非context.Background()启动子 goroutine,确保能被外部控制
channel 关闭到底谁负责?
发送方关,仅此一家。接收方只读、不关;多个 sender 共用一个 ch?说明设计错了,应该用 fan-in 模式或改用带缓冲 channel + timeout。
向已关闭的 channel 发送会 panic:panic: send on closed channel;从已关闭的 channel 接收会立刻返回零值 —— 这常被误认为“还有数据”,导致逻辑跳过清理。
- 生产者完成写入后,必须且只能调一次
close(ch) - 消费者用
for v := range ch是安全的,它会在 close 后自动退出 - 如果消费者不能依赖
range(比如要处理超时),就得自己select+ctx.Done()+,并手动判断是否因 close 返回零值
defer 在 goroutine 里根本不管用
defer 只在函数 return 时执行。如果 goroutine 里是 for {} 或卡在 ,defer 永远不会触发 —— 文件句柄、DB 连接、锁、大 slice 全部悬在内存里。
典型泄漏:go func(f *os.File) { defer f.Close(); for { f.Read(buf) } }(file) —— f.Close() 从不执行,fd 数很快打满。
- 资源释放逻辑必须和退出条件绑定,比如在
select的ctx.Done()分支里 close 文件、unlock、return - 用
sync.Once做一次性清理比 defer 更可靠,尤其对全局资源 - 轮询类 goroutine 必须有明确退出路径:超时、信号 channel、或由外部调用 stop 函数

















