直接用 go func(){} 会因无节制创建协程导致内存暴涨、调度过载甚至栈溢出崩溃;正确做法是用 buffered channel 实现协程池,限制并发数并确保 worker recover panic。

为什么直接用 go func() {} 会出问题
协程(goroutine)开销虽小,但无节制创建仍会导致内存暴涨、调度器过载,甚至 runtime: goroutine stack exceeds 1GB limit 这类崩溃。真实场景中,比如并发拉取 10 万条 API 数据,若每条都起一个 goroutine,瞬间上十万协程,调度器根本来不及切片,CPU 和内存反而被拖垮。
真正需要的不是“无限开”,而是“可控流”:限制同时活跃的协程数,让任务排队等空闲 worker 处理。
常见错误现象包括:
- 程序启动后几秒就卡死或 OOM
-
pprof显示goroutine数量持续 >50k 且不下降 - HTTP 客户端报
too many open files(底层连接没及时复用/关闭)
用 buffered channel 实现最简协程池
不用第三方库,仅靠 Go 原生语法就能搭出可用的池子。核心是用一个带缓冲的 chan struct{} 当“许可证”,控制并发上限;每个 worker 从任务队列(chan func())取活干。
实操建议:
- 许可证 channel 缓冲大小即最大并发数,例如
make(chan struct{}, 100)表示最多 100 个任务并行 - 任务 channel 不要设缓冲(或设小缓冲),避免任务积压在内存里不触发执行
- worker 必须用
for range持续读取任务,否则拿完一个就退出 - 记得在所有 worker 启动后 close 任务 channel,否则 range 永远阻塞
pool := make(chan struct{}, 100)
tasks := make(chan func(), 1000) // 缓冲可略大,防生产太快
<p>// 启动 worker
for i := 0; i </p><p>// 提交任务
for i := 0; i </p><h3>worker 函数必须处理 panic,否则整个池子会静默失效</h3><p>只要任意一个 worker 内部 panic 且未 recover,该 goroutine 就退出,池子可用 worker 数逐渐减少,最终任务全部堵在 channel 里——没有错误日志,没有崩溃,只有越来越慢。</p><p>所以每个 worker 的执行体必须包一层 <code>recover</code>:</p>
- 不要只在最外层 defer recover,要放在
task()调用前后 - panic 信息建议打到日志,至少输出函数名和错误类型,否则排查无从下手
- 别忽略 recover 后的错误,更不要用
_ = recover()掩盖问题
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("worker panic: %v", r)
}
}()
for task := range tasks {
pool <h3>实际工作中,任务函数常需返回结果或上下文,别硬塞闭包变量</h3><p>很多人写 <code>for i := range data { go func() { do(i) }() }</code>,结果所有 goroutine 都用到了最后一个 <code>i</code> 值。协程池里同样危险:若把任务参数通过外部变量传进闭包,极易发生数据竞争或错乱。</p><p>正确做法是把参数显式传入函数值:</p>
- 定义任务类型为
func(context.Context) error或func() (interface{}, error) - 提交时用
func() { do(data[i]) }立即捕获当前值,而不是共享变量 - 如果需收集结果,用带缓冲的
chan接收,别用全局 slice + mutex —— channel 本身已线程安全 - 注意 context 超时传递,避免某个慢任务拖垮整批处理
容易被忽略的一点:池子 shutdown 时,要等所有正在运行的 task 结束,再 close result channel,否则接收方可能漏数据或 panic on closed channel。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











