根本原因是goroutine泄漏、channel阻塞或context未传递导致调度失控;pprof查到大量runtime.gopark或select卡住即为泄漏,须确保每个goroutine监听ctx.done()、channel操作配超时、用worker pool硬限并发。

Go 服务在 QPS 超过 10000 后出现延迟飙升、内存持续增长甚至 OOM,大概率不是 Goroutine 本身不够快,而是调度失控、资源未回收或通信阻塞。关键不是“能不能并发”,而是“能不能稳住”。
goroutine 泄漏:pprof 查到的 runtime.gopark 栈太多就该警觉
泄漏不是“没写 return”,而是 goroutine 卡在无法退出的等待上。最常见三类:
- 向无人接收的
chan int写入(尤其无缓冲 channel) -
context.Context被 cancel 后,goroutine 仍忽略ctx.Done()继续循环 - 调用阻塞式系统调用(如未设超时的
http.Get、net.Dial)且未绑定ctx
定位命令必须带 ?debug=2:
curl 'http://localhost:6060/debug/pprof/goroutine?debug=2' | grep -A 5 -B 5 'your_handler_name'
看到大量重复栈顶是 runtime.gopark 或 selectgo,基本可判定泄漏。修复不是加 time.Sleep,而是确保每个 select 至少有一个 case 分支。
channel 阻塞:缓冲大小 ≠ 并发上限,写满仍会卡死
用 make(chan int, 100) 不代表能安全塞 100 个值——如果消费者宕机或处理太慢,第 101 次写入就会永久阻塞 goroutine。
- 生产者侧必须配超时:
select { case ch - 消费者不能只
for range ch,要配合ctx:for { select { case x, ok := - 缓冲区大小建议按「峰值每秒任务数 × 平均处理耗时」估算,而非拍脑袋设 1024
无缓冲 channel 仅适合严格一对一同步场景(如初始化握手),线上高并发服务慎用。
worker pool 限流:用 chan struct{} 控制并发数比 sync.WaitGroup 更可靠
sync.WaitGroup 只管“等结束”,不管“别起太多”。真正压住并发量的是信号量模式:
sem := make(chan struct{}, 10) // 最多 10 个并发
for i := range tasks {
sem <p>这个模式的关键点:</p>
- channel 类型必须是
struct{}(零内存占用) - 获取令牌放在
go前,确保启动前已占位 - 释放必须用
defer+ 匿名函数包裹,避免 panic 导致令牌不归还 - 不要把
sem传进 goroutine 内部再操作,易出竞态
超过 10 个任务会自然阻塞在 sem ,不额外消耗 CPU 轮询。
context 传递失效:下游函数没接收 ctx 参数就是埋雷
哪怕只有一层调用漏掉 context.Context,整条链路的超时和取消就全失效。典型错误:
-
func doDBQuery(id int) error—— 应改为func doDBQuery(ctx context.Context, id int) error - 调用时写
doDBQuery(context.Background(), id)而非复用上游传入的ctx - HTTP handler 里用
http.DefaultClient,没换用&http.Client{Timeout: ...}或传ctx
所有 I/O 操作(DB、HTTP、RPC、文件读写)都必须支持 context.Context。第三方库不支持?要么换库,要么自己包一层加 select 超时逻辑。
真正难的不是写出并发代码,而是让十万 goroutine 在流量高峰时各自安分守己——不抢锁、不堵 channel、不赖着不走。监控 runtime.NumGoroutine() 的趋势线,比任何 benchmark 都更能说明问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











