《Go 并发任务池 gopoolx:从 Panic 到优雅关闭》

0 阅读6分钟

gopoolx:一个真正工程化的 Goroutine 并发任务池

在 Go 项目里,并发几乎是逃不开的话题:批量 HTTP 请求、批量数据库操作、消息投递、任务编排……
很多团队的第一反应是“直接 go func() 干就完了”,结果项目一上量就会遇到这些问题:

  • goroutine 数量失控,内存飙升
  • 一堆散落在各处的 WaitGroup / err 处理,代码极难维护
  • 超时、取消逻辑混乱,有的任务该停不停,有的任务被误杀
  • 某个任务 panic 直接把整个进程打挂,线上背锅

为了解决这些工程上的“坑”,我做了一个小而精的库:gopoolx


1. gopoolx 是什么?

一句话概括:

gopoolx 是一个工程级的 Goroutine 并发任务池,用于在 Go 项目中安全、可控、高效地执行大量并发任务。

它不是单纯的“协程池 demo”,而是从下面这些真实诉求出发设计出来的:

  • 我只希望后台同时跑 N 个任务,不要无限开 goroutine
  • 所有任务都要挂在一个 context 上,支持业务级的超时/取消
  • 失败的任务可以按规则重试,不需要每个地方都自己写 for-loop
  • 错误要能统一收集,方便日志和告警
  • 有些任务需要有返回值,能像 Future 一样等结果
  • 不希望因为一个任务 panic,导致整个 worker 泄露或进程崩溃

2. 核心能力一览

gopoolx 当前已经支持:

  • 固定大小 worker 池

    • 通过 gopoolx.New(workerNum, ...) 限制并发度
    • 彻底告别“goroutine 爆炸”
  • 统一的 context 控制

    • 所有任务签名都是 func(ctx context.Context) error
    • 支持超时、手动取消等统一控制
  • 失败自动重试 + 间隔控制

    • WithRetry(n):失败后最多重试 n
    • WithRetryDelay(d):每次重试之间间隔多久
  • 集中错误收集

    • 所有任务错误会自动汇总到 pool.Errors()
    • 不用到处写 errs = append(errs, err)
  • panic 自动恢复

    • 普通任务和有返回值的任务内部 panic 都会被捕获
    • 转为 error 收集,不会打爆整个 worker
  • Future 泛型返回值

    • 使用 SubmitWithResult 提交有返回值任务
    • 通过 Future[T].Get(ctx) 获取结果,支持超时/取消
  • 非阻塞提交模式

    • WithNonBlocking() 开启后,队列满时直接丢弃新任务
    • 不阻塞调用方,也不会让 Wait() 卡死

仓库地址再放一遍,欢迎 star 😊:
github.com/hyin49954/g…


3. 快速上手:5 行代码跑起来

3.1 固定大小池 + 重试 + 统一错误收集

下面这个例子模拟批量执行任务,并对失败任务做自动重试,最后统一输出错误:

package main

import (
    "context"
    "log"
    "time"

    "github.com/hyin49954/gopoolx"
)

func main() {
    // 创建一个最多 5 个 worker 的协程池
    pool := gopoolx.New(
        5,
        gopoolx.WithRetry(2),                    // 失败最多重试 2 次
        gopoolx.WithRetryDelay(200*time.Millisecond), // 每次重试间隔 200ms
        gopoolx.WithQueueSize(100),              // 任务队列长度 100
    )

    // 全局 ctx,可以统一超时或者取消
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // 启动 worker
    pool.Run(ctx)

    // 提交一批任务
    for i := 0; i < 1000; i++ {
        pool.Submit(func(ctx context.Context) error {
            // 这里写你的业务逻辑:HTTP 调用 / DB 操作 / MQ 消费 ...
            return nil
        })
    }

    // 等待所有任务完成
    pool.Wait()

    // 统一处理错误
    for _, err := range pool.Errors() {
        log.Println("task error:", err)
    }
}

3.2 有返回值任务:像 Future 一样用

很多时候,我们不仅希望任务执行,还希望拿到返回结果。
gopoolx 内置了基于泛型的 Future 支持:

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/hyin49954/gopoolx"
)

func main() {
    pool := gopoolx.New(3)
    ctx := context.Background()

    pool.Run(ctx)

    // 提交一个返回 int 的任务
    f1 := gopoolx.SubmitWithResult(pool, func(ctx context.Context) (int, error) {
        time.Sleep(time.Second)
        return 100, nil
    })

    // 提交一个返回 string 的任务
    f2 := gopoolx.SubmitWithResult(pool, func(ctx context.Context) (string, error) {
        return "hello gopoolx", nil
    })

    // 在需要的地方等待结果(可以结合 ctx 控制超时/取消)
    v1, _ := f1.Get(ctx)
    v2, _ := f2.Get(ctx)

    fmt.Println(v1, v2)

    pool.Wait()
}

如果任务内部发生 panic,会被自动捕获并作为 error 返回给 Future,而不会把整个 worker 弄挂。

3.3 非阻塞模式:高吞吐场景的保险丝

在某些高 QPS 场景下,你可能更希望“宁可丢一些任务,也不要把调用方堵死”。
这时候可以启用非阻塞提交模式:

pool := gopoolx.New(
    10,
    gopoolx.WithQueueSize(1000),
    gopoolx.WithNonBlocking(), // 队列满时直接丢弃新任务
)

pool.Run(context.Background())

for {
    pool.Submit(func(ctx context.Context) error {
        // 短任务
        return nil
    })
}
  • 队列未满:任务正常入队,worker 执行,错误会被收集
  • 队列已满:Submit 直接返回,该任务会被丢弃
    • 不会阻塞调用方
    • 不会导致 WaitGroup 计数异常
    • 也不会出现在 pool.Errors()

这一模式适合:

  • 日志/埋点这类可丢任务
  • 一些“降级路径”:系统已经很忙了,只要主流程不被拖死即可

4. 设计上的一些小心思

4.1 统一的执行路径:executeWithRetry

所有提交到池里的任务,最终都会经过一个统一的执行入口:

  • 负责失败重试
  • 负责重试间隔
  • 负责 panic 恢复并记录错误

这样做的好处:

  • 重试、恢复逻辑集中管理,不会在业务代码里散成一坨
  • 新增能力(比如统计重试次数、上报 metrics)也只需要在一个地方改

4.2 ErrorCollector:并发安全的错误收集

错误收集器做了两件事:

  • 内部用 sync.Mutex 确保并发安全
  • Errors() 返回的是切片副本,调用方可以放心地在外面修改

这保证了在高并发场景下,你拿错误列表做日志/告警,不会对池内部状态造成破坏。

4.3 Future[T]:类型安全的结果获取

相较于“在外部定义 chan interface{} 自己玩”,gopoolx 的 Future:

  • 使用 Go 泛型,Future[int] / Future[string] 这种一看就知道类型
  • 内部有一个 done 通道,只有在任务结束(成功/失败/panic)时才会关闭
  • Get(ctx) 支持通过 ctx 控制等待时间,避免无休止阻塞

5. 和手写 goroutine 相比,有什么优势?

很多人会说:“这些我自己用 go func + WaitGroup 写也不难”。
的确,能不能写出来不是问题,问题在于:

  • 每个地方都要重新搭一遍“轮子”:WaitGroup、错误切片、重试 for-loop、panic recover…
  • 不同同事、不同模块的写法不一样,bug 很难排查
  • 当要统一加一类能力(比如统计、限流、埋点),改动面会非常大

gopoolx 做的事情就是:

  • 把这些通用的并发管理逻辑统一收口
  • 让业务代码只关心:我写一个 func(ctx context.Context) error 就行
  • 其他的交给池来做:并发度、重试、错误收集、panic 恢复、返回值获取…

6. 后续计划 & 欢迎参与

目前 gopoolx 已经能稳定支撑不少常见场景,后续会考虑:

  • 更丰富的监控指标(成功/失败/重试/丢弃统计)
  • 更细粒度的限流/优先级控制
  • 和常见观测系统(Prometheus 等)的集成示例

如果你在使用过程中有任何想法或问题,欢迎到 GitHub 提 Issue / PR:

也非常欢迎直接在掘金评论区交流:
你在 Go 并发实践中踩过哪些坑?有没有什么你希望在任务池里“开箱即用”的能力?
说不定下一个 feature 就是为你写的。