直接起大量goroutine会因内存占用高、调度器压力大及压垮下游服务而出问题;用带缓冲channel可实现并发池,以容量控制最大并发数。

为什么直接起大量goroutine会出问题
Go 的 goroutine 虽轻量,但不是免费的:每个默认栈约 2KB(可增长),上万 goroutine 就吃掉几十 MB 内存;调度器压力增大,runtime.schedule 耗时上升;更常见的是下游服务被打爆——比如并发调用 HTTP 接口,没节制地起 5000 个 goroutine,目标服务可能直接 503。
用带缓冲 channel 实现最简并发池
核心思路是用 channel 做“令牌桶”:容量即最大并发数,每次任务前先 拿令牌,执行完再 sem 归还。不依赖第三方库,零依赖,适合快速嵌入。
常见错误写法:sem := make(chan struct{}, 10) 后忘记初始化填充——必须提前塞满,否则第一个 就阻塞:
sem := make(chan struct{}, 10)
for i := 0; i < cap(sem); i++ {
sem <- struct{}{}
}使用时注意:
立即学习“go语言免费学习笔记(深入)”;
- 别在 goroutine 内部漏写
defer func() { sem ,panic 时令牌回不来 - 不要把
sem传给多个不相关的任务组,它们会共享同一池子,互相挤占 - 缓冲大小设太小(如 1)等于串行,太大(如 1000)失去控流意义
用 sync.WaitGroup + channel 控制任务生命周期
单纯靠 sem 只能限并发,没法等所有任务结束。必须配 sync.WaitGroup,且 WaitGroup.Add() 必须在发任务前调用,不能在 goroutine 里调——否则竞态或漏计数。
典型结构:
var wg sync.WaitGroup
sem := make(chan struct{}, 10)
for i := 0; i < cap(sem); i++ {
sem <- struct{}{}
}
for _, task := range tasks {
wg.Add(1)
go func(t Task) {
defer wg.Done()
<-sem
defer func() { sem <- struct{}{} }()
t.Run()
}(task)
}
wg.Wait()
注意点:
- 闭包捕获
task要传值(如(task)),别直接用循环变量t,否则所有 goroutine 用的是最后一个值 -
WaitGroup和sem是两个独立控制维度:前者管“是否结束”,后者管“能否开始” - 如果任务有返回值或错误,需额外 channel 收集结果,不能只靠
WaitGroup
生产环境建议用 worker pool 模式而非简单限流
上面方案适合一次性批量任务,但若要长期运行、支持动态扩缩容、任务优先级或超时控制,就得升级成 worker pool:启动固定数量的长驻 goroutine,从任务队列取活干。这样避免反复创建销毁开销,也更容易监控吞吐和积压。
关键差异:
- channel 类型从
chan struct{}变成chan Task(任务队列) - worker 数量在启动时确定,不再靠
sem动态抢锁 - 任务提交方只往队列发,不感知 worker 状态;worker 自己循环
,失败可重试或丢弃 - 要加
context.Context支持整体取消,否则关不掉正在跑的 worker
最容易被忽略的是:worker pool 的任务队列 channel 如果无缓冲且没人消费,jobs 会永久阻塞调用方。务必确保 worker 已就绪再投任务,或用带缓冲的 jobs := make(chan Task, 100) 防止雪崩。










