用带缓冲的chan struct{}实现信号量可控制并发goroutine数量:通道容量即最大并发数,每个goroutine启动前先获取令牌(<-ch),完成后释放(ch<-struct{}{})。

用 semaphore 控制并发 goroutine 数量最直接
Go 没有内置的“最大并发数”开关,但用带缓冲的 chan struct{} 实现信号量(semaphore)是标准做法:它轻量、无锁、语义清晰。本质是把“可用并发槽位”变成通道容量,每个 goroutine 启动前先 拿一个槽,结束时 <code>sem 归还。
常见错误是把缓冲通道当成普通 channel 用,比如写成 make(chan int, 10) 却往里塞数字——这会破坏信号量语义,且容易因类型不匹配 panic。必须用 struct{},零内存占用,纯粹作计数器。
- 初始化:
sem := make(chan struct{}, maxConcurrent),maxConcurrent是你真正想限制的并发数(比如 5 或 20) - 启动 goroutine 前:
sem (阻塞直到有空槽) - goroutine 结束时(务必在 defer 或结尾):
(释放槽位) - 别用
len(sem)判断剩余容量——它只反映当前缓冲区已填数量,不是实时可用槽数;应始终靠 channel 阻塞机制来调度
用 sync.WaitGroup + sync.Mutex 组合也能做,但没必要
有人尝试用 WaitGroup 记总数、Mutex 保护计数器变量来限流。这能跑通,但引入了锁竞争和额外状态管理,性能差、易出错。尤其在高并发下,Mutex 成为瓶颈,而 channel 的调度由 runtime 优化过,更高效。
典型误用场景:在循环里反复 mu.Lock(); if count ,再启 goroutine —— 这里存在竞态窗口(check-then-act),即使加锁也难保证原子性;而 <code>sem 是原子的获取+阻塞操作。
立即学习“go语言免费学习笔记(深入)”;
- 除非你在调试或教学演示,否则不要手写计数器+锁的方案
-
WaitGroup应只用于等待所有 goroutine 结束,不该参与并发控制逻辑 - 如果真要用状态变量,至少用
atomic.Int64,但依然不如 channel 简洁可靠
注意 context.Context 超时与限流的配合时机
限流只是控制“同时跑几个”,不解决“单个任务卡死怎么办”。所以实际使用中,必须把 context.WithTimeout 或 context.WithCancel 和 semaphore 结合:先拿槽,再套 context 启动任务。
错误顺序是先启 goroutine 再传 context——此时槽位已被占用,但任务可能永远不结束,导致后续请求全被堵住。正确顺序是:拿槽 → 创建带超时的 context → 执行任务 → 归还槽位。
- 别在 goroutine 外部调用
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)后直接传进 goroutine——要确保每个 goroutine 拥有自己的独立 context 实例 - 归还槽位的
必须放在 <code>defer里,且在 context 取消后仍能执行,否则槽位泄露 - 如果任务本身支持 cancel(如
http.Client),记得把ctx传进去,否则 timeout 不生效
第三方库如 golang.org/x/sync/semaphore 更安全但有版本依赖
Go 官方扩展包 x/sync/semaphore 提供了封装好的 Weighted 信号量,支持带权重的获取(比如某些任务占 2 个槽),内部做了更多边界检查(如负权重 panic、超额释放等)。但它不是标准库,需额外 go get,且 Go 1.21+ 才默认兼容模块版本。
如果你的项目已用 module,且团队接受外部依赖,用它比手写 channel 更省心;但如果只是简单限流,原生 chan struct{} 足够,零依赖、可读性强、runtime 层面最贴近底层调度。
- 初始化:
sem := semaphore.NewWeighted(int64(maxConcurrent)) - 获取:
if err := sem.Acquire(ctx, 1); err != nil { /* 超时或取消 */ } - 释放:
sem.Release(1) - 注意
Acquire返回 error,必须检查;而原生 channel 方式是阻塞或 panic(比如向已关闭 channel 发送),行为更明确
真正容易被忽略的是:限流值不是越大越好。设成 CPU 核心数的 2–3 倍通常是合理起点,但最终得看任务类型——IO 密集可稍高,CPU 密集必须压低,否则 GC 压力和上下文切换开销会反噬吞吐。上线前务必用真实负载压测,而不是凭感觉设 100。


















