裸用go关键字发任务不靠谱,因无限流、无法等待、panic静默丢失、无重试与错误回调;需用WaitGroup+Context控制生命周期,或内存队列+指数退避实现可靠异步任务。

为什么直接用 go 关键字发任务不靠谱
Go 的 go 语句启动协程确实轻量,但裸用它做“任务分发”很快会失控:没有限流、无法等待完成、panic 会静默丢失、也没有重试或错误回调。真实业务里,你面对的是 HTTP 请求触发任务、定时批量处理、或者下游服务临时不可用——这些场景下,一个只管 go f() 的实现连日志都来不及打就崩了。
- 协程泄漏常见于循环中忘记控制并发数,比如
for _, item := range items { go process(item) },items 有 10 万条?那就会起 10 万 goroutine - 没有上下文传递,超时和取消完全失效
- 错误被吞掉:协程内 panic 不会传播到主 goroutine,
recover()得手动加,还容易漏 - 无法统一监控任务成功率、延迟、积压量
用 sync.WaitGroup + context.Context 控制生命周期
这是最轻量但足够可靠的起点。核心是把“发任务”和“等结果”解耦,同时让每个任务可取消、可超时。
func DispatchWithContext(ctx context.Context, tasks []func(context.Context) error, maxConcurrent int) error {
var wg sync.WaitGroup
sem := make(chan struct{}, maxConcurrent)
errCh := make(chan error, len(tasks)) // 缓冲避免阻塞
<pre class="brush:php;toolbar:false;">for _, task := range tasks {
wg.Add(1)
go func(t func(context.Context) error) {
defer wg.Done()
sem 0 {
return fmt.Errorf("tasks failed: %v", errs)
}
return nil}
-
maxConcurrent必须设,否则就是裸go;建议从 4–16 开始调,别硬写 100 -
sem用 channel 实现信号量比sync.Mutex更适合协程调度 -
errCh缓冲大小设为len(tasks),避免第一个失败就卡住整个流程 - 别在 task 里忽略
ctx.Err(),尤其涉及 HTTP、DB、time.Sleep 时
当任务需要持久化和重试:引入内存队列 + 简单 backoff
如果进程挂了任务不能丢,就得落地。先不急着上 Redis 或 Kafka——很多内部服务用内存队列 + 定期 checkpoint 就够用,且可控性更强。
- 用
list.List做 FIFO 队列,配合sync.RWMutex读写保护 - 每个任务结构体至少含:
id string、fn func() error、retryCount int、nextRetryAt time.Time - 启动一个常驻 goroutine 轮询队列:
for range time.Tick(100 * time.Millisecond),只取nextRetryAt.Before(time.Now())的任务 - 重试间隔用指数退避:
time.Second ,上限封顶到 5 分钟
常见坑:
- 不加锁直接遍历
list.List会 panic:“concurrent map iteration” 类似错误 - 重试逻辑写在分发侧还是执行侧?推荐写在执行侧(即 task 函数内部判断
retryCount ),分发侧只管推入队列 - checkpoint 文件写入要原子:先写
queue.tmp,再os.Rename(),避免读到半截数据
别过早抽象成“引擎”,先看你的任务到底要不要并发
很多号称“异步任务引擎”的需求,其实只是想把耗时操作从 HTTP handler 里摘出去。这时候最简方案是:
// 全局一个带缓冲的 channel
var taskQueue = make(chan func(), 1000)
<p>func init() {
go func() {
for task := range taskQueue {
task()
}
}()
}</p><p>func FireAndForget(task func()) {
select {
case taskQueue </p>
- 这比任何库都快、没依赖、没配置项
- 如果任务本身有 I/O,这个串行消费反而更稳(避免 DB 连接池被打爆)
- 只有当你明确需要:并行执行、优先级调度、跨进程分发、精确失败统计——才值得引入 worker pool、消息中间件或完整框架
真正难的不是并发模型,是定义清楚“任务成功”的语义:是函数返回 nil?还是 DB 写入确认?还是第三方回调到达?这个边界模糊时,再多的 goroutine 也救不了设计缺陷。











