用带缓冲的 chan struct{} 实现信号量最轻量、最符合 go 风格;第三方 semaphore 包(如 x/sync/semaphore)额外支持上下文取消和细粒度计数,简单限流直接用 make(chan struct{}, n) 即可。

用 semaphore 包还是自己写 chan 控制?
Go 没有内置 Semaphore 类型,但用带缓冲的 chan struct{} 实现信号量最轻量、最符合 Go 风格。第三方 semaphore 包(比如 golang.org/x/sync/semaphore)本质也是封装了 channel,但多了上下文取消支持和更细粒度的计数控制。如果你只做简单并发限制(比如最多 5 个 HTTP 请求同时跑),直接用 make(chan struct{}, 5) 就够了;需要超时或取消时,再考虑 x/sync/semaphore。
常见错误是把 chan int 或 chan bool 当信号量用——语义不清,还浪费内存;struct{} 零大小,纯占位,最干净。
-
make(chan struct{}, N)初始化后,往里发struct{}{}即“获取许可”,从里面收一次即“释放许可” - 别用
len(ch)判断剩余容量——它返回当前已发送未接收的数量,不是“空闲槽位数”;正确方式是看cap(ch) - len(ch) - 如果多个 goroutine 同时争抢,channel 自带阻塞和 FIFO 保证,不用额外加锁
http.Client 并发请求限流的实际写法
限制 HTTP 请求并发数,核心是在发起 Do() 前先“拿令牌”,完成后“还令牌”。不能放在 http.Transport 层去配 MaxIdleConnsPerHost——那只是连接复用限制,不等于并发请求数。
典型场景:批量调用第三方 API,怕打爆对方或触发限频。这时候每个请求都走独立 goroutine,但必须串行拿 token。
立即学习“go语言免费学习笔记(深入)”;
- 初始化:
sem := make(chan struct{}, 10)表示最多 10 个并发 - 每个请求前:
sem (阻塞直到有空位) - 请求结束后(包括 error 分支):
(必须确保执行,建议用 <code>defer) - 别在
for range循环里直接起 goroutine +sem ,容易死锁——要先确保 channel 有足够容量,或用带缓冲的结构预分配
x/sync/semaphore 什么时候值得引入?
当你需要响应 context.Context 取消、或者想精确控制“等待获取许可”的超时时间时,golang.org/x/sync/semaphore 就比裸 channel 更合适。它的 Acquire() 方法接受 context.Context,被 cancel 或 timeout 后会立即返回错误,不会卡住 goroutine。
但注意:这个包的 Acquire() 是“尝试获取 N 个单位”,不是单个;默认单位是 1,但你可以传 int64(2) 表示一次占两个槽位——适合资源不对等的场景(比如大文件上传比小查询更耗资源)。
- 初始化:
sem := semaphore.NewWeighted(5) - 获取:
if err := sem.Acquire(ctx, 1); err != nil { return } - 释放:
sem.Release(1)(必须和 Acquire 的数值匹配) - 错误信息可能是
context.Canceled或context.DeadlineExceeded,需显式判断处理
别忽略 panic 和 recover 场景下的泄漏
用 defer sem.Release(1) 看似稳妥,但如果 goroutine 在 Release 前 panic 且没被 recover,defer 不会执行——信号量就永远卡住一个槽位。这在长期运行的服务里会缓慢耗尽并发能力。
真实项目中,HTTP handler、数据库查询、外部 RPC 调用都可能 panic(比如 JSON 解析失败、空指针解引用)。不能只依赖 defer。
- 对关键路径,用
recover()包一层,在defer之后再补一次Release - 或者改用
tryAcquire模式:先非阻塞检查是否可获取,再真正执行业务;失败就跳过,不进 goroutine - 上线前用压测工具故意注入 panic(比如在 handler 里随机
panic("test")),验证信号量是否稳定归还
信号量本身不难,难的是所有退出路径都被覆盖。尤其当逻辑嵌套深、error 处理分支多的时候,少写一行 或漏掉一个 <code>Release,问题就会慢慢堆起来。










