用chan struct{}实现信号量最直接:make(chan struct{}, n)创建n容量缓冲通道,取许可用

用 semaphore 控制并发请求数量最直接
Go 没有内置信号量,但用 sync.Mutex + sync.Cond 或更简单的 chan struct{} 就能实现轻量级信号量。核心思路是:用带缓冲的 channel 当“许可证池”,每次发请求前取一个,用完放回。
常见错误是把 channel 声明成 chan int 或 chan bool——没必要,struct{} 零内存开销,语义也更清晰。
- 初始化:
sem := make(chan struct{}, 10)表示最多 10 个并发 - 获取许可:
sem (阻塞直到有空位) - 释放许可:
(必须在 defer 或 error 处理后执行,否则泄漏) - 别用
len(sem)判断剩余容量——它返回当前已占用数,不是可用数;可用数是cap(sem) - len(sem)
HTTP 请求里嵌套 sem 的典型写法
不是给整个 HTTP client 加锁,而是对每个请求入口做控制。比如你有一组 URL 要并发抓取,每个 goroutine 在真正调用 http.Do() 前先过信号量。
容易踩的坑是把 sem 放在 goroutine 外部循环里,导致只限流了启动速度,没限制实际并发连接数;或者忘了 recover panic 后没释放许可,造成死锁。
- 正确位置:goroutine 内部、
http.NewRequest之后、client.Do()之前 - 务必用
defer func() { ,而不是裸写 <code>,防止 panic 时漏释放 - 如果请求超时或失败,许可仍要归还——
defer正好覆盖这个逻辑 - 示例片段:
go func(url string) { sem
http.Transport.MaxConnsPerHost 和信号量的区别
两者都影响并发,但作用层不同:MaxConnsPerHost 是底层 TCP 连接复用的硬上限,属于连接池管理;而信号量控制的是“同时发起的请求数”,不管它们是否复用连接。
如果你设了 MaxConnsPerHost=5,但用信号量开了 20 并发,结果是:前 5 个请求建新连接,后面 15 个会排队等空闲连接——这反而可能放大延迟,还掩盖了真实瓶颈。
- 推荐组合:信号量设为业务可接受的最大并发数(如 10),
MaxConnsPerHost设为略大于它(如 15),避免连接争抢 -
MaxIdleConns和MaxIdleConnsPerHost也要同步调大,否则 idle 连接被提前回收,每次都要重建 - 注意:这些 transport 设置只对复用 client 生效;每次 new http.Client 都得重新配
调试时怎么确认信号量真起作用了
光看程序不 panic 不代表限流生效。常见假象是:日志显示 20 个 goroutine 启动了,但实际并发只有 3–4 个——可能因为 DNS 解析慢、服务端响应慢,或信号量根本没被所有路径覆盖。
最靠谱的方式是加计数器 + 日志打点,而不是依赖肉眼观察 goroutine 数量。
- 在
sem 前打印当前 <code>len(sem),能看出排队长度突增 - 用
runtime.NumGoroutine()辅助判断,但注意它包含 runtime 管理的 goroutine,不能直接等同于活跃请求 - 如果发现
len(sem)长期等于cap(sem),说明下游处理太慢,信号量成了瓶颈,得优化 handler 或扩容 - 别依赖
pprof的 goroutine profile 看“并发数”——它拍的是快照,而信号量阻塞是瞬时状态
实际用下来,最难把握的是信号量大小和下游承载力的匹配。设小了吞吐上不去,设大了触发服务端熔断或 429。上线前最好用真实 endpoint 做阶梯压测,而不是只在本地 mock 上验证逻辑。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











