推荐用golang.org/x/sync/semaphore实现带超时的并发控制,其weighted类型支持acquire(ctx, n)阻塞等待并响应超时或取消,必须配对release且defer保障panic安全,不可用于qps限制而应配合rate.limiter。

用 semaphore 包实现带超时的并发控制
Go 标准库没有内置信号量,但用 sync.WaitGroup + chan struct{} 手写容易漏掉超时、取消、重入等边界情况。推荐直接用社区验证过的 golang.org/x/sync/semaphore —— 它专为并发数限制设计,支持上下文取消和超时等待。
安装:go get golang.org/x/sync/semaphore
关键点:
-
semaphore.Weighted是核心类型,构造时传入最大并发数(如semaphore.NewWeighted(5)) - 获取许可必须用
Acquire(ctx, 1),不能忽略返回的error—— 超时或上下文取消都会在这里报错 - 必须配对调用
Release(1),哪怕函数 panic 也要用defer保证释放,否则信号量永久泄漏 - 权重不一定是 1:比如一个任务占 2 个单位资源,就传
Acquire(ctx, 2),适合异构任务场景
Acquire 返回 context.DeadlineExceeded 怎么处理?
这不是 bug,是正常流程。当并发池已满且等待超时,Acquire 返回 ctx.Err(),常见值包括 context.DeadlineExceeded 和 context.Canceled。
典型错误写法:if err != nil { log.Fatal(err) } —— 这会让整个程序退出。
正确做法:
- 检查错误是否来自上下文:
errors.Is(err, context.DeadlineExceeded) - 根据业务决定是跳过任务、降级处理,还是返回 HTTP 429
- 不要把信号量错误和业务错误混在一起处理;它属于“资源调度失败”,不是“数据出错”
为什么不用 chan struct{} 手写信号量?
手写看似简单,但实际踩坑率极高:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 无超时机制:原生 channel 等待无法中断,一旦阻塞就卡死
- 无法动态调整容量:
make(chan struct{}, N)的 N 是固定的,扩容需重建 channel 并迁移等待者 - 释放逻辑易出错:忘记
close()或重复close()会 panic;用select配合default又可能跳过获取 - 无统计能力:不知道当前有多少 goroutine 在排队、多少已获取、平均等待时长等,线上排障困难
semaphore.Weighted 内部用 sync.Cond 实现,所有操作线程安全,且每个 Acquire 调用都可独立 cancel。
HTTP 服务中限制每秒请求数(QPS)能用这个吗?
不能直接用 semaphore.Weighted 控制 QPS —— 它管的是「同时运行的 goroutine 数」,不是「单位时间请求数」。
比如设为 10,并发 10 个慢请求(每个耗时 10s),那这 10 秒内新请求全被拒绝,但 QPS 实际只有 1;反之,10 个快请求(10ms 完成),1 秒内就能跑上千 QPS。
要控 QPS,请用:golang.org/x/time/rate.Limiter(令牌桶),配合 limiter.Wait(ctx)。
两者常组合使用:先用 rate.Limiter 控制入口流量速率,再用 semaphore.Weighted 控制下游资源并发上限,避免雪崩。
信号量真正难的不是写,而是想清楚“限制的是什么资源”——CPU?内存?数据库连接?外部 API 配额?不同资源的瓶颈维度完全不同,硬套一个数字反而掩盖问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










