Go 里所谓“goroutine 内存泄漏”,本质是 goroutine 卡在 select、time.Sleep 或 channel 操作等阻塞点而永不退出,持续占用栈空间、调度开销及闭包变量,导致这些资源无法被 GC 回收。

Go 里所谓“goroutine 内存泄漏”,不是堆内存没被 GC 回收,而是 goroutine 卡在 select、、<code>time.Sleep 或 http.Get 上永不返回——它持续占着栈(至少 2KB)、调度器跟踪开销、以及闭包捕获的所有变量,这些对象全不能被 GC。
goroutine 卡在 channel 操作上不退出
这是最常见泄漏点:无缓冲 channel 发送阻塞、有缓冲 channel 满了没人收、receiver 提前退出但 sender 还在发。
- 向
ch发送前,必须用select包一层,带default或ctx.Done()分支,避免永久挂起 - receiver 不要只写
for range ch就完事——得确认 sender 真的会close(ch);否则 range 永远等不到 EOF - 谁创建
ch,谁负责close();向已关闭的 channel 发送会 panic,接收则立即返回零值 - 错误示例:
go func() { for i := 0; ; i++ { ch ——第一次发送就卡死,<code>i和闭包环境全锁在内存里
time.Ticker / time.Timer 没 stop 导致资源残留
time.NewTicker 和 time.NewTimer 返回的对象底层持有运行时计时器资源,不显式 Stop() 就算 goroutine 退出了,资源也不会释放。
-
defer ticker.Stop()必须写,且要写在 goroutine 函数体开头附近,不能依赖 “反正有 context 控制” - 别用
time.After替代time.NewTimer做长周期等待——time.After创建的 timer 不可回收,哪怕你 never 读它,也会一直占着资源直到超时触发 - 正确写法:
ticker := time.NewTicker(1 * time.Second); defer ticker.Stop(); for { select { case
HTTP handler 或数据库查询没传 context
裸调 http.Get、db.Query、io.Read 等 I/O 操作,一旦网络卡住或服务无响应,goroutine 就永远停在 syscall 上,既不响应 cancel,也不释放连接。
立即学习“go语言免费学习笔记(深入)”;
- 所有 I/O 必须用支持 context 的变体:
http.DefaultClient.Do(req.WithContext(ctx))、db.QueryContext(ctx, sql)、conn.SetReadDeadline配合ctx.Done()轮询 -
defer resp.Body.Close()不够——如果resp根本没拿到(比如请求卡在 DNS 或 TCP 握手),Close()压根不会执行;要用ctx控制整个请求生命周期 - 第三方库也要查文档:比如
redis-go的Do方法不支持 context,得换DoContext
sync.WaitGroup 计数错乱或 defer 失效
WaitGroup 本身不防泄漏,它只管“你声称要等几个”,但不管“它们到底退没退”。更危险的是,很多人以为 defer 能兜底清理,其实它只在函数 return 时触发。
- goroutine 里写
for { ... }死循环 +defer f.Close()?那f.Close()永远不执行,文件描述符、DB 连接、锁全泄漏 -
wg.Add(1)必须在go前调用;wg.Done()必须在 goroutine 最终退出路径上(包括 panic 后 recover 的分支) - 不要靠
runtime.NumGoroutine()断言“应该只剩 main”,它只反映数量,不反映状态;真正要看的是/debug/pprof/goroutine?debug=2里那些停在chan receive或semacquire的 goroutine
最易被忽略的一点:goroutine 的退出条件必须是**主动可检的**,而不是“靠别人关 channel”或“等主程序崩”。每个 go 语句启动前,先问一句:它靠什么退出?超时?ctx.Done()?channel 关闭?还是某次 read 返回 io.EOF?没答案,就先别起。


















