推荐使用 sync.Semaphore(Go 1.21+)或 golang.org/x/sync/semaphore(旧版本),因其安全、可中断、panic 友好;手写 make(chan struct{}, N) 虽轻量但易因漏 defer 导致卡死。

直接用 sync.Semaphore(Go 1.21+)或 golang.org/x/sync/semaphore(旧版本),比手写带缓冲通道更安全、可中断、panic 友好;若只是简单脚本且兼容老版本,make(chan struct{}, N) 能用,但漏 defer 就卡死。
用 sync.Semaphore 控制并发(推荐,Go 1.21+)
sync.Semaphore 是标准库原生支持的加权信号量,专为并发数限制设计,不依赖外部包,且自动处理上下文取消和 panic 安全释放。
- 初始化必须传
int64:如sem := semaphore.NewWeighted(int64(5)),传0或负数会 panic - 获取许可必须检查错误:
if err := sem.Acquire(ctx, 1); err != nil,常见错误是context.DeadlineExceeded或context.Canceled - 释放必须配对且用
defer:defer sem.Release(1),否则 panic 或提前 return 会导致令牌永久泄漏 - 别在 handler 内部重复 new
semaphore——它应是全局单例,生命周期与服务一致
用 golang.org/x/sync/semaphore 兼容旧版本
Go 1.20 及更早版本没有 sync.Semaphore,此时应使用官方维护的 golang.org/x/sync/semaphore,行为和接口几乎一致,只是需手动安装:
- 执行
go get golang.org/x/sync/semaphore - 初始化:
sem := semaphore.NewWeighted(5)(注意这里参数是int64,但 Go 1.20 不强制类型转换,仍建议显式写int64(5)) -
Acquire和Release的调用逻辑与标准库完全相同,同样必须defer Release - 若要快速失败(不等、不阻塞),可用
sem.TryAcquire(1),返回bool,适合限流后直接返回 HTTP 429 场景
用 make(chan struct{}, N) 手写信号量(轻量但高风险)
所有 Go 版本都支持,零依赖,但极易出错。它本质是把 channel 当作“许可证池”,容量即最大并发数,struct{} 零内存开销且语义清晰。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 初始化:
sem := make(chan struct{}, 3)表示最多 3 个任务并发 - 获取许可:
sem (阻塞直到有空位),**不能用 <code>select+default替代,否则可能跳过限流** - 释放许可:
,**必须放在 <code>defer中,且 defer 必须在 goroutine 内部注册**,闭包变量如i要显式传参,否则所有 goroutine 共享同一个值 - 别用
len(sem)判断是否可进——它返回已占用数,不是剩余数;真正可用的是cap(sem) - len(sem),但一般没必要查 - 绝对不要
close(sem),后续读写都会 panic;也不要用chan int或chan bool,纯属浪费内存和混淆语义
HTTP 请求并发限制必须落在 client.Do() 前一刻
很多人把信号量放在 goroutine 启动前或 for 循环外,结果只限制了 goroutine 创建速度,实际 http.Client 并发请求数仍爆表。真正有效的限流点只有一个:
- 每个请求的 goroutine 内部、
http.NewRequest()之后、client.Do()之前 - 即使请求失败、超时或 panic,也要归还许可——
defer正好覆盖这个逻辑 - 别混淆
http.Transport.MaxConnsPerHost:它管连接复用,不等于并发请求数;设成 5 却开 20 并发,只会让 15 个请求排队等连接,延迟放大且掩盖瓶颈 - 如果任务本身支持 context(如
client.Do(req.WithContext(ctx))),记得把同一个ctx传给Acquire和业务逻辑,确保取消能同时作用于调度层和执行层
最常被忽略的点是:信号量只控制“同时跑几个”,不负责任务排队、超时熔断、指标统计;一旦出现长期 hang 住的任务(比如第三方接口不响应),光靠信号量无法主动清理,这时候就得上 worker pool(如 ants)——但那是另一层问题了。

















