★ goroutine 池怎么实现?context 在真实服务中如何传递?
一句话结论:goroutine 池 = 有界 worker + 任务队列 + 优雅退出:用带缓冲 channel 当队列,N 个 worker 循环取任务执行,关闭队列并等 worker 全部退出;若只是想限制并发数,一个带缓冲 channel 当信号量就够了。context 则约定为函数第一个参数,负责传递取消信号、deadline 与只读元数据(request id/trace),从 handler 一路透传到 DB/RPC,保证“取消能向下传导、goroutine 不泄漏”。
goroutine 池两种实现
// 方式一:信号量限流(最轻量)
var sem = make(chan struct{}, 10)
for task := range tasks {
sem <- struct{}{} // 满则阻塞等待空位
go func(t Task) {
defer func() { <-sem }()
t.Run(ctx)
}(task)
}
// 方式二:固定 worker 池
func RunPool(ctx context.Context, n int, jobs <-chan Job) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case j, ok := <-jobs:
if !ok {
return // 队列关闭,正常退出
}
j.Run(ctx)
case <-ctx.Done():
return // 被取消,立即退出
}
}
}()
}
wg.Wait() // 等所有 worker 收尾
}
要点:worker 数可配置(CPU 密集约等于 CPU 核数,IO 密集调大);退出靠 close(jobs) 与 ctx 双通道;父 goroutine 用 wg.Wait 等待全部结束,避免“通道关了但 worker 还没干完”就返回。
context 的使用规范
- 约定:ctx 是函数第一个参数,类型 context.Context,不作为结构体字段长期保存。
- 传递三类信息:取消信号(Done)、截止时间(Deadline)、只读元数据(Value:request id、用户、trace id)。
- 服务端链路:middleware 创建 ctx 并注入 request id,往下传到 service,访问 DB/RPC 时用 context.WithTimeout 派生超时并 defer cancel()。
- 只要把 ctx 传给了 goroutine 与下游,取消就会级联:客户端断开、上游超时都能立刻终止整条链,防止 goroutine 泄漏。
func handle(r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel() // 无论成功失败都要释放
rows, err := db.QueryContext(ctx, sql)
// 下游 RPC 同样把 ctx 传进去
}
context.Value 的纪律
- 只放“请求级、跨层必需”的元数据,别当万能参数袋;业务参数走函数参数。
- Value 查找要沿链遍历,高频热点别滥用;语义上要求只读、只传向下游。
常见追问 / 记忆点
- 追问:池里的 worker 如何感知服务要下线?——先 cancel(ctx) 再 close(jobs),worker 在 select 里退出,wg.Wait 确保收尾完成。
- 追问:context 不取消会怎样?——带超时的到期才释放,没超时的可能一直挂着,时间一长就是 goroutine 泄漏。
- 记忆点:池 = 队列 + worker + 双通道退出;ctx 第一个参数、defer cancel、取消级联、勿存勿滥用 Value。