★ 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。
笔记加载中…