直接用 go func() 易拖垮系统,因goroutine非免费:每协程占2KB栈内存,大量启动耗尽资源且增调度开销;更致命的是无视下游QPS限制致雪崩。

为什么直接用 go func() 容易把系统拖垮
协程(goroutine)虽轻量,但不是免费的:每个协程至少占用 2KB 栈空间,大量无节制启动会迅速耗尽内存;调度器压力增大后,runtime.schedule() 开销上升,反而降低吞吐。更现实的问题是——你根本不知道下游服务能扛住多少并发,比如调用一个 QPS 上限为 50 的 HTTP 接口,开 1000 个 goroutine 纯属制造雪崩。
用 chan + for range 实现最简 worker pool
核心思路是复用固定数量的 goroutine,从任务队列中持续取活干。关键不是“池”,而是“阻塞式消费”:
type Job struct {
ID int
Data string
}
func worker(id int, jobs <-chan Job, results chan<- string) {
for job := range jobs {
// 模拟耗时处理
result := "worker-" + string(rune('0'+id)) + ":" + job.Data
results <- result
}
}
func main() {
const numWorkers = 4
jobs := make(chan Job, 100)
results := make(chan string, 100)
// 启动固定数量 worker
for w := 0; w < numWorkers; w++ {
go worker(w, jobs, results)
}
// 提交任务
for j := 0; j < 10; j++ {
jobs <- Job{ID: j, Data: "task"}
}
close(jobs) // 关闭 jobs 才能让 worker 退出循环
// 收集结果(注意:需确保所有任务已提交再关闭)
for a := 0; a < 10; a++ {
fmt.Println(zuojiankuohaophpcn-results)
}}
-
jobs是带缓冲的 channel,避免生产者被阻塞(但缓冲区不宜过大,否则失去流控意义) - 必须
close(jobs),否则for range jobs永不退出,worker 泄漏 - 结果 channel 不要 close,由接收方控制读取次数(或用
sync.WaitGroup配合无缓冲 channel)
用 ants 库替代手写池子的适用场景
如果你需要动态扩缩容、任务超时控制、panic 恢复、或集成 metrics,别重复造轮子。官方推荐的 ants(github.com/panjf2000/ants/v2)比手写更健壮:
立即学习“go语言免费学习笔记(深入)”;
- 默认使用
sync.Pool复用 worker 结构体,减少 GC 压力 -
Submit()返回error,可区分“池满拒绝”和“执行失败” - 设置
Options.Nonblocking = true时,任务提交失败立即返回,不阻塞调用方 - 注意:
ants.NewPool(100)创建的是 *固定* 数量 worker,不是最大值;想实现弹性伸缩得自己包装一层
典型误用:pool.Submit(func(){...}) 中直接捕获不到 panic —— 必须在提交的函数内部用 defer/recover,因为 panic 发生在 worker goroutine 内部。
协程池里怎么安全传参和共享状态
常见错误是把外部变量地址直接闭包进 goroutine,导致竞态:
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 全是 5!
}()
}正确做法只有两种:
- 传参:
go func(val int) { ... }(i) - 局部变量:
val := i; go func() { ... }()
若需共享状态(如计数器、缓存),必须加锁或用原子操作:sync.Map 适合读多写少,atomic.Int64 适合简单计数,千万别用普通 map + map[xxx] = yyy 在多个 worker 间乱写。
真正容易被忽略的是 context 传递——所有涉及 I/O 的任务(HTTP 请求、DB 查询)都该接收 ctx context.Context 参数,并在 worker 内部用 ctx.Done() 响应取消,否则池子可能卡死在某个慢请求上。











