Go中goroutine泄漏比误用sync.Pool更常见,主因是任务卡住、panic未recover、channel关闭不当;应使用buffered channel+WaitGroup实现固定大小协程池,worker数需按IO/CPU密集型区分设置。
goroutine 泄漏比池子没用好更常见
Go 里没有内置的 goroutine 池,直接用
管理 goroutine 是错的——它只适合复用对象,不负责调度或生命周期控制。真正要防的是:任务提交后 goroutine 卡住、panic 未 recover、channel 关闭不及时,导致 goroutine 永远挂起。
常见错误现象:
下跑着跑着卡死;pprof 查
数持续上涨;日志里反复出现
。
永远在 worker goroutine 里加
,尤其处理用户传入的函数时
用带缓冲的
(比如
),避免 submit 阻塞导致调用方协程堆积
池关闭时,先关 input channel,再
,最后关 result channel —— 顺序反了就漏 goroutine
用 buffered channel + waitGroup 实现最小可行池
不需要引入第三方库,15 行内能写出生产可用的固定大小池。核心是把“任务分发”和“worker 生命周期”拆开:channel 负责排队,
负责等待退出,
控制关闭幂等性。
使用场景:HTTP handler 中批量调第三方 API、日志异步刷盘、定时任务批量重试。
立即学习
“
go语言免费学习笔记(深入)
”;
示例关键结构:
worker 数设成 runtime.NumCPU() 多数时候是错的
CPU 密集型任务才该对齐 CPU 核数;IO 密集型(HTTP、DB、文件)反而需要更大池子,否则大量 goroutine 在 sysmon 里等网络就绪,实际并发度上不去。
参数差异:
控制 P 的数量,影响调度器吞吐,和 worker 数无关
池大小建议从
起调,压测时看
里 goroutine block 和 syscall wait 时间占比
超过 500 个 worker 且任务轻量(<1ms)时,channel 争用会成为瓶颈,考虑改用
分片任务队列
别让 context.WithTimeout 包裹整个 Submit 调用
常见错误是把超时套在
外层,结果任务进队列成功,但还没执行就超时返回——用户以为任务丢了,其实它还在池里排队。
正确做法:超时控制必须下沉到 task 内部,或者用带 deadline 的 channel select:
性能影响:每次 Submit 做 timeout select 有微小开销,但比丢任务或阻塞调用方强得多。如果池满是常态,说明任务处理不过来,该扩容或降级,而不是掩盖问题。
容易被忽略的是:worker 里执行的函数如果自己没接
,timeout 就完全无效——池子只管调度,不管业务逻辑怎么跑。
sync.Poolruntime.GOMAXPROCS(1)goroutinefatal error: all goroutines are asleep - deadlock!recover()chan *taskmake(chan *task, 100)waitGroup.Wait()sync.WaitGroupsync.Oncetype Pool struct {
tasks chan *task
workers int
wg sync.WaitGroup
once sync.Once
}
func (p *Pool) Submit(f func()) {
p.tasks <- &task{fn: f} // 不阻塞,靠 buffer 抗峰
}
func (p *Pool) startWorker() {
defer p.wg.Done()
for t := range p.tasks { // channel 关闭后自动退出
if t != nil && t.fn != nil {
defer func() { recover() }()
t.fn()
}
}
}runtime.GOMAXPROCS(n)50go tool pprof sync.Mappool.Submit(...)select {
case p.tasks <- t:
case <-time.After(2 * time.Second):
return errors.New("pool full or busy")
}context.Context