gopoolx:一个真正工程化的 Goroutine 并发任务池
在 Go 项目里,并发几乎是逃不开的话题:批量 HTTP 请求、批量数据库操作、消息投递、任务编排……
很多团队的第一反应是“直接 go func() 干就完了”,结果项目一上量就会遇到这些问题:
- goroutine 数量失控,内存飙升
- 一堆散落在各处的
WaitGroup/err处理,代码极难维护 - 超时、取消逻辑混乱,有的任务该停不停,有的任务被误杀
- 某个任务 panic 直接把整个进程打挂,线上背锅
为了解决这些工程上的“坑”,我做了一个小而精的库:gopoolx。
- GitHub 仓库地址: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 就是为你写的。