Go并发是需实战验证的工程能力,核心在于安全、可维护、可伸缩;goroutine泄漏难发现,常因channel关闭后仍有等待或循环无退出机制导致内存持续增长,pprof中runtime.gopark占比高是典型信号。

Go 的并发不是“学完 goroutine 和 channel 就能用好”的技能,而是需要在真实压力场景中反复验证、调整和踩坑的工程能力。进阶阶段的核心问题不是“怎么写”,而是“为什么这么写才安全、可维护、可伸缩”。
goroutine 泄漏比 panic 更难发现
泄漏通常发生在 channel 关闭后仍有 goroutine 在等待接收,或循环中无条件启动 goroutine 却没配对的退出机制。它不会立刻崩溃,但内存持续增长,pprof 查到 runtime.gopark 占比异常高就是典型信号。
- 永远避免
go func() { 这类没有超时、没有关闭检查的裸接收 - 用
select配合default或time.After做兜底,比如select { case v := - 所有长期运行的 goroutine 必须监听
ctx.Done(),并在收到信号后 clean up 并 return
channel 缓冲区大小不是拍脑袋决定的
无缓冲 channel(make(chan int))强制同步,适合严格顺序控制;有缓冲 channel(make(chan int, 100))能解耦生产/消费速率,但缓冲区过大等于掩盖背压问题,过小又频繁阻塞。
- I/O 密集型任务(如 HTTP 请求转发)常用小缓冲(1–10),靠限流控并发
- 批处理流水线(如日志聚合)可设为预期单批次最大量,避免内存暴涨
- 永远别用
len(ch) == cap(ch)判断“满”来决策——这不可靠,且破坏 channel 的原子语义
sync.WaitGroup 不是万能等待器
WaitGroup 只保证 goroutine 启动后执行完毕,不保证执行逻辑正确。常见误用是 Add 在 goroutine 内部调用,或 Done 被多次调用导致 panic。
立即学习“go语言免费学习笔记(深入)”;
- Add 必须在
go之前调用,且数值确定——不能在循环里动态 Add 却漏掉某次 - 推荐封装模式:
var wg sync.WaitGroup; for _, job := range jobs { wg.Add(1); go func(j Job) { defer wg.Done(); work(j) }(job) } - 若需等待带返回值的 goroutine,优先用 channel 汇总结果,而非靠 WaitGroup + 全局变量
context.WithCancel 的 cancel 函数必须显式调用
很多人以为 defer cancel() 就万事大吉,但若 goroutine 已因其他原因提前退出,cancel 函数未被调用,子 context 就永远存活,关联的 timer、goroutine、内存都泄漏。
- cancel 函数应在所有可能退出路径上确保执行(包括 error return、panic recover 后)
- 不要把 cancel 传给不可信代码——它可能被意外调用,导致上游提前终止
- 用
context.WithTimeout替代手动管理 timer + cancel,更不容易漏掉
真正卡住人的从来不是语法,而是当 1000 个 goroutine 同时读写一个 map、channel 被 close 后仍有人往里 send、或者 WaitGroup 计数错 1 时,程序既不 panic 也不报错,只是慢得无法解释——这时候,race detector 和 pprof 不是可选工具,是必需品。



















