直接用go启动goroutine不够用,因其缺乏统一调度、错误隔离、限流和重试机制,易导致服务打爆、panic崩溃、goroutine泄漏、无背压及不可观测等问题。

为什么直接用 go 启动 goroutine 不够用
因为缺乏统一调度、错误隔离、限流和重试机制。比如批量调用第三方 API 时,100 个 go request() 可能瞬间打爆目标服务,或某个 panic 导致整个程序崩溃;也没有办法控制并发数、记录失败任务、或在失败后自动重试。
真正需要的是一个带缓冲、可控制、有状态反馈的队列层,而不是裸奔 goroutine。
- goroutine 泄漏:任务函数里忘了 recover 或阻塞未处理,goroutine 永远卡住
- 无背压:生产者 push 太快,内存暴涨(尤其用无缓冲 channel)
- 不可观测:不知道当前积压多少任务、成功/失败率、耗时分布
用 sync.WaitGroup + channel 实现基础可控队列
这是最轻量、不依赖外部库的实现方式,适合中小规模(千级以内任务)、对可靠性要求不极端的场景。
核心结构是带缓冲的 chan 做任务入口,固定数量 worker 从 channel 消费,WaitGroup 控制生命周期:
立即学习“go语言免费学习笔记(深入)”;
type Task func()
type WorkerPool struct {
tasks chan Task
workers int
}
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workers; i++ {
go func() {
for task := range wp.tasks {
defer func() {
if r := recover(); r != nil {
log.Printf("task panic: %v", r)
}
}()
task()
}
}()
}
}
func (wp *WorkerPool) Submit(task Task) {
select {
case wp.tasks <- task:
default:
log.Println("task dropped: queue full")
}
}
-
wp.tasks必须带缓冲(如make(chan Task, 100)),否则Submit会阻塞生产者 -
select {... default: ...}是防积压的关键,避免生产者被拖垮 - 每个 worker 内必须加
defer recover(),否则单个 panic 会让整个 goroutine 退出,worker 数逐渐归零
用 github.com/hibiken/asynq 处理需持久化与重试的任务
当任务不能丢(如支付回调、邮件发送)、需要延迟执行、失败后自动重试、或跨进程恢复时,纯内存队列就不够了。asynq 基于 Redis,提供任务去重、优先级、TTL、失败重试策略等生产级能力。
典型使用流程:
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: "localhost:6379"},
asynq.Config{Concurrency: 10},
)
mux := asynq.NewServeMux()
mux.HandleFunc("send_email", SendEmailHandler)
srv.Run(mux)
// 提交任务
client := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"})
, = client.Enqueue(asynq.NewTask("send_email", payload))
- handler 函数必须返回
error,asynq 才会根据错误决定是否重试 - 默认最大重试 10 次,可通过
asynq.MaxRetry(3)覆盖 - Redis 宕机时,
Enqueue会报错,必须显式处理(比如降级到本地文件暂存) - 不要在 handler 里传闭包或非序列化值——payload 必须是 JSON 可序列化的 map/slice/primitive
并发数设多少才合理
没有通用答案,取决于任务类型和资源瓶颈。盲目调高 Concurrency 或 worker 数反而降低吞吐。
- CPU 密集型(如图像压缩):通常设为
runtime.NumCPU(),再加 1–2,再多就是上下文切换开销 - IO 密集型(如 HTTP 请求):可设为 10–100,但要配合目标服务的限流(比如对方只允许 5 QPS,你开 50 并发毫无意义)
- 数据库写入:受连接池限制,
sql.DB.SetMaxOpenConns(n)和 worker 数要匹配,否则大量dial tcp: lookup xxx: no such host或超时
最稳妥的方式是先设保守值(如 4),压测观察 CPU、网络、目标响应时间、错误率,再逐步上调——别信“越多越好”。










