直接用 go 关键字并发过多会导致内存暴涨、调度压力大、压垮下游;协程池用于控并发、复用 goroutine、防毛刺,适用于http批量转发、db写入等场景,需避免闭包捕获、合理设池大小、安全关闭。

为什么直接用 go 关键字不够用
并发任务一多,go func() {...}() 会无节制创建 goroutine,内存涨得快,调度器压力大,还可能把下游服务打崩。协程池不是为了“炫技”,是为控制并发数、复用 goroutine、避免瞬时毛刺。
常见错误现象:runtime: goroutine stack exceeds 1000000000-byte limit 或 CPU 突增但实际吞吐没提升——本质是调度混乱,不是代码写得慢。
- 场景明确:HTTP 请求批量转发、数据库批量写入、文件并发处理
- 别池化单次短任务(比如纯计算),开销反超收益
- 池大小不是越大越好;通常设为
runtime.NumCPU() * 2起步,再根据压测调
worker 和 task 怎么定义才不翻车
核心就两条:task 必须可序列化(或至少不带闭包引用),worker 必须能反复执行不同 task。很多人把 func() 直接塞进 channel,结果闭包捕获了循环变量,所有 task 执行的都是最后一次值。
正确做法是让 task 成为一个结构体或接口,显式传参:
立即学习“go语言免费学习笔记(深入)”;
<pre class="brush:php;toolbar:false;">type Task struct {
Fn func()
Err error
}
worker 循环里只取 task、执行、清字段,不依赖外部作用域:
- 避免在 task 中直接引用 for 循环的
i 或 <code>v,改用func(v T) { ... }(v)立即捕获 - 如果 task 需要返回结果,用
chan Result或回调函数,别靠全局变量或共享 map - worker 启动前应检查
task.Fn != nil,防止 panic 波及整个池
如何安全关闭协程池并等任务结束
关池不是杀 goroutine,而是拒绝新任务 + 等待运行中任务自然退出。用 sync.WaitGroup + chan struct{} 组合最稳。
关键点在于:worker 在收到关闭信号后,必须先处理完当前 task,再退出;不能“中途丢弃”。否则数据丢失、状态不一致。
- 关闭流程三步走:关闭
taskCh→wg.Wait()→ 关闭doneCh - worker 的 for-select 里必须有
case 分支,且该分支放在 <code>default或case task := 之后,确保不漏 task - 别用
time.Sleep等 worker 退出,不可靠;必须依赖wg.Done()配合wg.Wait()
要不要加任务超时和重试机制
协程池本身不负责超时,那是 task 层的事。强行在池里统一加 context.WithTimeout 容易误伤正常长任务,也违背“池只管调度”的职责边界。
更合理的分层是:task 自己决定是否带 context,worker 只做透传。重试同理——由业务逻辑封装,不是池的义务。
- 如果 80% 的 task 都需要 5s 超时,就在 task 构造时统一加
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) - 池的
Submit方法签名保持简单:func(t Task) error,不掺杂 ctx、retry 等参数 - 真正容易被忽略的是:worker panic 后没 recover,会导致 goroutine 消失且无提示——务必在 worker 内部包一层
defer func() { recover() }()










