推荐使用 sync.semaphore(go 1.21+)或 golang.org/x/sync/semaphore(旧版本),因其支持上下文取消、panic 安全释放;手写 chan struct{} 易因漏 defer 导致永久阻塞,且 len(sem) 返回已占用数而非可用数,不可靠。

直接用 sync.Semaphore(Go 1.21+)或 golang.org/x/sync/semaphore(旧版本),别手写 chan struct{} 控制并发连接数——它容易漏 defer 导致连接句柄卡死、无法响应超时、也不支持上下文取消。
为什么不能用 len(sem) 判断是否能建新连接
常见错误是写 if len(sem) 。这看似“检查空位”,实则危险:<code>len(sem) 返回的是当前已占用数,不是可用数;而且它是快照值,多 goroutine 竞争下拿到的瞬间值立刻失效。哪怕你看到 len(sem) == 0,下一纳秒就可能被其他 goroutine 占满。
正确做法永远是直接调用 sem.Acquire(ctx, 1) 或往 sem 发送——依赖阻塞机制本身做协调,而不是靠“猜”。
-
cap(sem) - len(sem)才是理论可用数,但不可用于决策逻辑 - HTTP 客户端里误用这个判断,常导致连接数远超预期,触发服务端限流或 TCP 耗尽
- 一旦用了
len()做分支,就等于绕过信号量的同步语义,退化成竞态条件
sem.Acquire 必须在 http.NewRequest 之前调用
限流位置错,等于没限。很多人把信号量放在 goroutine 启动前、甚至 for 循环外,结果只是减慢了任务派发节奏,实际打开的连接数仍爆表。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
真正要控制的是“同时处于活跃连接状态”的数量,也就是从 http.NewRequest 开始、到 resp.Body.Close() 结束这一整段生命周期。
- 正确位置:
sem.Acquire(ctx, 1)紧接在http.NewRequest之后、client.Do(req)之前 - 必须配对
defer sem.Release(1),且写在 goroutine 函数最开头,确保 panic 或 early return 也能释放 - 错误示范:
for range urls { go func() { sem.Acquire(...) } }—— 这里sem在循环外,只控启动速度,不控连接数
http.Transport 参数和信号量不是一回事
MaxConnsPerHost、MaxIdleConns 管的是底层 TCP 连接复用池,不是并发请求数。设成 5 并不意味最多发 5 个请求;它只表示最多复用 5 个空闲连接。如果你用信号量开了 20 并发,而 MaxConnsPerHost=5,结果就是前 5 个建新连接,后面 15 个排队等空闲连接——延迟放大,还掩盖真实瓶颈。
- 推荐组合:
sem设业务可接受的最大并发数(如 10),MaxConnsPerHost设略大一点(如 15) -
MaxIdleConns和MaxIdleConnsPerHost也要同步调大,否则 idle 连接被提前回收,每次都要重建握手开销 - 这些 transport 设置只对复用的
http.Client生效;每次 new client 都会绕过连接池
最易被忽略的一点:信号量许可是“租约”,不是全局资源。Acquire 成功才产生有效租约,Release 必须由同一 goroutine 调用,且参数必须与 Acquire 申请数量严格一致。Acquire 返回 context.Canceled 后还调 Release,会 panic;Acquire(3) 后拆成三次 Release(1),计数器虽不崩,但语义已错——后续监控和等待逻辑全不可信。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










