直接用 goroutine 会崩是因为不加限制地启动大量 goroutine 会导致调度争抢、内存暴涨,甚至触发 oom;关键在控制同时运行数量而非启动总数。

为什么直接用 goroutine 会崩?
不加限制地启动成百上千个 goroutine,轻则触发调度器争抢、内存暴涨,重则触发 runtime: out of memory 或被系统 OOM kill。Go 的并发不是“越多越好”,而是“够用且可控”。关键不在“启多少”,而在“同时跑几个”。
常见错误是写成这样:
for _, item := range items {
go process(item) // ❌ 没有节流,items 一万个就起一万个 goroutine
}
这本质上不是批处理,是并发轰炸。
用带缓冲 channel 实现最简 WorkerPool
核心思路:用一个 chan struct{} 当“许可证桶”,容量即最大并发数;每个 worker 启动前先从桶里取一张“票”,处理完再还回去。
实操建议:
- 定义池结构体,包含
jobs(输入任务 channel)、results(输出结果 channel)、sem(信号量 channel)和workers数量 -
sem初始化为make(chan struct{}, maxConcurrent),别用int类型——避免整数溢出和竞争 - 每个 worker 循环中先执行
阻塞拿许可,处理完再 <code>sem 归还 - 务必在
defer或显式位置关闭results,否则接收方可能永远阻塞
示例关键片段:
sem := make(chan struct{}, 10) // 最多 10 并发
for i := 0; i <h3>用 errgroup + semaphore 更稳妥</h3><p>原生 <code>errgroup.Group</code> 不自带并发控制,但配合 <code>golang.org/x/sync/semaphore</code> 就很干净。它比手写 channel 更健壮:支持带权重的获取、可取消、能返回具体错误。</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig"><img
src="https://img.php.cn/upload/skill/000/000/081/179215391842073.jpg" alt="Golang Uber Dig" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="overflowclass">Golang Uber Dig</a>
<p class="overflowclass">使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等</p>
</div>
<a rel="nofollow" href="/xiazai/skill8661" title="Golang Uber Dig" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>使用场景:</p>
- 需要统一捕获第一个 panic 或 error
- 任务耗时差异大,想避免某个长任务卡死整个池
- 后续可能扩展为带 context 取消的批处理
注意点:
-
semaphore.Weighted的Acquire是阻塞操作,必须传入非零context,推荐用context.Background()或带超时的 - 每个
Acquire后必须配对Release,哪怕发生 panic,建议用defer sem.Release(1) - 权重设为
1即等价于传统工作池;设为2表示该任务占两个并发槽位
简例:
sem := semaphore.NewWeighted(5)
g, ctx := errgroup.WithContext(context.Background())
for _, job := range jobs {
job := job // 避免循环变量复用
g.Go(func() error {
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
defer sem.Release(1)
process(job)
return nil
})
}
_ = g.Wait()
别忽略任务分发与结果收集的边界问题
并发度控制只是冰山一角。真正线上出问题的,往往是任务分发没关 channel、结果读取没做超时、或 panic 没 recover 导致 worker 消失。
容易踩的坑:
-
jobschannel 必须由生产者 close,否则 worker 会永远range等下去;别指望靠并发数自动结束 - 接收
results时,如果任务数不确定或部分失败,别用固定for i := 0; i ,改用 <code>for range+select超时 - worker 内部没包
recover(),一个 panic 就少一个 worker,池子慢慢变“残废” - 大量小任务时,channel 通信开销可能反超处理本身,这时应考虑批量打包(如每 10 个合成一个 job)再投递
复杂点从来不在“怎么限并发”,而在于“谁负责关 channel、谁负责收 panic、谁保证结果不丢”。这些地方漏一个,压测时就掉链子。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










