go协程泄漏最常因无退出路径、缺失context或未关闭channel导致,典型如for range未关channel或select无超时,协程永久阻塞在chan receive,2kb栈内存无法被gc回收。

Go 里用 go func() 启协程,不加控制几乎必 leak——不是“可能”,是“大概率已经 leak 了”。关键不在语法对不对,而在退出路径有没有、上下文有没有、资源有没有被带走。
goroutine 泄漏最常卡在 chan receive 和 select 上
匿名函数一旦启动,就脱离调用栈生命周期;如果它阻塞在未关闭的 channel 上,或没设超时的 select,就会永远挂起,栈内存(默认 2KB)持续占着,GC 完全无能为力。
- 典型错误写法:
go func() { for v := range ch { process(v) } }()——ch永远不关,这个 goroutine 就永远不死 - HTTP handler 中常见陷阱:
go func() { —— <code>time.After创建的 timer 不会自动回收,且 handler 返回后 goroutine 仍在运行 - 修复核心:所有
go func()必须绑定context.Context,并在select中监听ctx.Done(),确保退出有兜底
sync.Pool 不是缓存,别往里塞闭包或带指针的对象
很多人把 sync.Pool 当成通用对象池,往里放闭包、结构体指针、甚至 map[string]*BigStruct,结果 GC 压力不降反升——因为 Pool 里的对象不会被 GC 扫描,只要还在 Pool 里,其引用链上的所有内存都活得好好的。
- 正确用法只限短生命周期、创建开销大的值类型对象:比如
[]byte、bytes.Buffer、预分配的 struct(字段不含指针) - 闭包本身会捕获外部变量,若闭包被 Put 进 Pool,它捕获的变量也一并被“冻结”在堆上,极易形成隐式泄漏
- Put 前必须重置对象状态:比如清空
bytes.Buffer的buf字段,否则下次 Get 出来还带着旧数据,可能引发逻辑错误或内存膨胀
pprof 差分看 alloc_space,比看 inuse_space 更早发现泄漏苗头
单次 go tool pprof http://localhost:6060/debug/pprof/heap 很难看出问题,因为泄漏对象在总量中占比小;真正有效的是跨时间抓两个快照做差分,盯住“新增分配最多”的地方。
- 抓取命令:
wget http://localhost:6060/debug/pprof/heap -O heap1.out,等 5–10 分钟再抓heap2.out - 对比命令:
go tool pprof -base heap1.out heap2.out,进交互后输入top或top -cum - 重点关注:
bytes.makeSlice、runtime.malg、net/http.readRequest—— 它们本身不 leak,但高频出现说明你在 hot path 上反复 new buffer / goroutine / request,是泄漏前兆
最隐蔽的泄漏点,往往藏在闭包捕获的变量逃逸到堆后,又被全局 map 或长期存活的 channel 持有——它不报错、不 panic、甚至 runtime.NumGoroutine() 看起来也正常。只有差分堆快照 + 调用栈下钻,才能揪出那个多持有一小时的 *User 指针。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











