应使用 semaphore.newweighted 进行业务层并发控制,而非 runtime.gomaxprocs;需全局单例初始化、检查 acquire 错误、defer 释放、避免负权重,并配合 numgoroutine 监控与 ctx 统一超时。

别用 runtime.GOMAXPROCS 控制并发数
runtime.GOMAXPROCS 只影响调度器能使用的 OS 线程(M)数量,和你的业务 goroutine 数量完全无关。设成 1 不会限制你起 10000 个 goroutine,设成 100 也不会自动帮你压住并发。它改的是底层并行度,不是业务层限流。
用 golang.org/x/sync/semaphore 做全局信号量
Go 1.21+ 直接用官方 semaphore.NewWeighted 是最稳妥的选择,它支持 context、可取消、无竞态泄漏风险。
- 初始化必须是全局单例:
var sem = semaphore.NewWeighted(50),别在 handler 里反复 new -
sem.Acquire(ctx, 1)必须检查 error:如果返回context.Canceled或context.DeadlineExceeded,直接跳过任务,别往下走 -
defer sem.Release(1)要写在 goroutine 最外层,哪怕 panic 也能触发释放 - 别传
0或负数给NewWeighted,会 panic;容量建议按服务典型 QPS × 平均耗时(秒)粗估,再留 20% 余量
监控 runtime.NumGoroutine() 防泄漏
光限流不够,得知道实际跑了多少 goroutine。每秒打一次点比只看峰值更有价值。
- 启动一个独立 goroutine 定期打印:
log.Printf("goroutines: %d", runtime.NumGoroutine()) - 如果数值持续缓慢上涨(比如每分钟 +5),大概率有 goroutine 卡在 channel receive、HTTP timeout 没设、或 defer 里没调
sem.Release - 别依赖
len(sem)查“剩余槽位”——它返回的是已占未放的数量,真正可用数是cap(sem) - len(sem),但一般没必要实时查 - 配合
debug.ReadGCStats看 GC 频次,突增往往意味着 goroutine 泄漏拖慢了内存回收
超时与 panic 场景下信号量不泄漏的关键细节
很多翻车发生在异常路径上:panic 没 recover、context 超时后还继续执行、HTTP client 没配 timeout。
- 所有阻塞操作(
http.Do、db.Query、)必须接受同一个 <code>ctx,不能只用在Acquire上 - 别在
select里只写case 就开干——万一后续 panic,<code>defer sem.Release(1)根本没注册上 - 如果要用
time.AfterFunc做兜底超时,它只能发信号,必须配合select { case 主动退出 -
sem.TryAcquire(1)适合快速失败场景(如 HTTP 429),但它不看 context,也不阻塞,成功才进业务逻辑
真正难的不是设个数字,而是让每个 goroutine 在任何退出路径(正常 return、panic、ctx cancel、timeout)下都归还令牌。这要求把 Acquire 和 Release 绑定到同一作用域,且 release 必须在 defer 中、在最外层。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











