goroutine泄漏比性能差更致命,因未回收goroutine导致内存持续上涨、GC压力增大、调度器拖垮;需用pprof定位阻塞栈,严格使用context取消、避免无条件启goroutine、合理控制channel缓冲。

goroutine 泄漏比性能差更致命
Go 程序并发变慢,十有八九不是调度器不够快,而是 goroutine 没被回收。常见于未关闭的 channel、阻塞的 select、或忘记 cancel 的 context。一旦泄漏,内存持续上涨,GC 压力增大,最终拖垮整个调度器——此时 runtime.NumGoroutine() 会稳定在异常高位,且不随业务请求下降。
- 用
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2
抓取活跃 goroutine 栈,重点关注长期处于chan receive或select状态的调用链 - 所有带超时/取消逻辑的 I/O 操作,必须使用
context.WithTimeout或context.WithCancel,且确保defer cancel()在函数入口就注册 - 避免在循环中无条件启
go func() { ... }();若必须,用带缓冲的channel控制并发数,或借助semaphore(如golang.org/x/sync/semaphore)
不要手动调大 GOMAXPROCS
GOMAXPROCS 默认值已是 runtime.NumCPU(),盲目设为更高数值不会提升吞吐,反而加剧 M(OS 线程)与 P(逻辑处理器)之间的上下文切换开销,尤其在高争用锁场景下,runtime.sched.lock 成为瓶颈。
- 仅当明确观察到 P 长期空闲(
pprof -top -cum显示大量schedule时间)、且 CPU 利用率远低于物理核数时,才考虑微调 - 生产环境建议保持默认;若需动态调整,用
runtime.GOMAXPROCS(n),但避免频繁变更(每次调用触发 STW) - 真正影响并发效率的是
goroutine的“轻量性”是否被破坏:比如在goroutine中执行同步磁盘 I/O、长循环计算、或调用阻塞 C 函数——这些会让 M 被抢占,P 被挂起,引发“M 膨胀”
channel 使用不当直接拖垮调度器
无缓冲 channel 的发送/接收是同步点,若配对操作缺失或顺序错乱,极易造成 goroutine 永久阻塞;而大容量缓冲 channel(如 make(chan int, 1e6))则隐式占用大量堆内存,GC 扫描压力陡增。
- 优先用无缓冲
channel做信号同步(如done := make(chan struct{})),它零内存分配、语义清晰 - 若需缓冲,容量必须有明确上限,且与业务吞吐匹配;例如处理日志时,
make(chan []byte, 1024)比make(chan []byte, 0)更安全,但绝不用make(chan []byte, 1 - 永远检查
channel是否已关闭:从已关闭的channel接收会立即返回零值,但发送 panic;用v, ok := 判断状态,而非依赖超时
sync.Pool 不是万能缓存
sync.Pool 适合复用临时对象(如 []byte、bytes.Buffer),但它的生命周期由 GC 控制,**不保证对象一定被复用**。若误把它当长期缓存用(比如存用户会话、数据库连接),不仅无法复用,还会因引用残留阻碍 GC,导致内存泄漏。
立即学习“go语言免费学习笔记(深入)”;
- 只用于高频创建/销毁的小对象;初始化
sync.Pool时必须设置New字段,否则 Get 返回 nil - 从 Pool 取出的对象,使用前必须重置(如
buf.Reset()),不能假设内容为空 - 不要跨 goroutine 共享
sync.Pool实例;每个逻辑模块应定义自己的 Pool,避免不同大小对象混入同一池,降低命中率
真正难优化的从来不是 goroutine 数量,而是每个 goroutine 里那几行没加 context、没设超时、没关 channel 的代码。











