goroutine 必须有明确退出路径,不能依赖 gc 回收;go 的 gc 不会回收阻塞中的 goroutine,哪怕它卡在 channel 操作、锁等待或死循环中,将导致内存和资源泄漏。

goroutine 必须有明确退出路径,不能靠 GC 回收
Go 的 GC 不会回收阻塞中的 goroutine。哪怕它卡在 或 <code>time.Sleep 上,栈、闭包变量、引用的对象全锁在内存里,runtime.NumGoroutine() 只增不减就是典型信号。
常见错误是写成:go func() { for { doWork(); time.Sleep(1 * time.Second) } }()——没出口,永远跑下去。协程启动前必须想清楚:靠什么停?超时?通道关闭?父 context 取消?没答案就别起。
- 所有长生命周期 goroutine 必须接收
context.Context参数,且第一个参数传入 - 阻塞操作(
、<code>ch 、<code>http.Do、db.Query)必须放进select,并带case -
defer在 goroutine 里只在函数 return 时触发;死循环或永久阻塞下,defer f.Close()根本不执行
channel 关闭责任必须清晰,receiver 别 assume sender 会 close
谁创建 channel,谁负责 close —— 这是硬规则。receiver 写 for range ch 前,得确认 sender 确实会在某处调用 close(ch);否则就是永久阻塞。
典型泄漏场景:ch := make(chan int); go func() { for i := 0; ; i++ { ch ,没人收,第一次发送就卡死,<code>i 和栈全留着。
- sender 若可能 panic 或提前退出,
close(ch)必须放在defer里(但注意:panic 时 defer 仍执行) - receiver 别依赖
len(ch) == 0判断可读性——不可靠,且无法解决阻塞问题 - 无缓冲 channel 发送前建议用
select包一层,加default或ctx.Done()分支防死锁
context.WithTimeout 配合 select 是标配,但漏监听等于没写
ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 本身不终止 goroutine,只是发一个信号。真正起作用的是你在 goroutine 里是否监听了 。
常见错误包括:闭包捕获外层已 cancel 的 ctx、for range ch 里没检查 ctx、调第三方库时用了 Query 而不是 QueryContext。
- 监听必须放在
select的第一个case,避免因其他 case 先就绪而错过取消信号 - 定时器类操作(
time.Ticker)必须显式ticker.Stop(),哪怕加了 ctx 控制也不能省略 - cancel 函数必须在启动 goroutine 的作用域里显式调用(比如 defer cancel()),漏掉这句等于没加
用 sync.WaitGroup + channel 关闭组合做主协程同步退出
main 函数不能靠 time.Sleep 等 goroutine 结束——时间不准,且无法保证全部退出。可靠方式是:channel 控制逻辑退出 + WaitGroup 等待实际结束。
粗糙版(close(ch) 后直接 return)会导致部分 goroutine 没来得及退出就被 main 结束;完美版必须等所有 goroutine 显式调用 wg.Done()。
- worker 函数开头调
wg.Add(1),结尾defer wg.Done() - main 中
close(ch)后必须跟wg.Wait(),确保所有 goroutine 已返回 - WaitGroup 不能和 channel 关闭混用不当:比如在 goroutine 里 close 自己接收的 channel,会导致 panic
最易被忽略的点:pprof 看到几百个状态为 chan receive 的 goroutine,基本就是泄漏源头;但真正难定位的是那些分配量小、alloc_space 增量高却 inuse_space 不显眼的泄漏——差分分析比盯单次快照管用得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











