用带缓冲的channel实现信号量最直接,初始化为sem := make(chan struct{}, maxconcurrency),获取许可需写入channel,释放需读取,常见错误是容量设为0或忘记释放导致泄漏。

用 semaphore 控制并发 goroutine 数量最直接
Go 标准库没提供原生信号量,但用 sync.Mutex + sync.Cond 或 chan struct{} 都能实现轻量级限流。推荐用带缓冲的 channel:它天然阻塞、语义清晰、无锁开销。
常见错误是把 channel 容量设成 0(变成同步 channel),导致所有 goroutine 立即阻塞;或忘记在 goroutine 结束后释放 token,造成永久泄漏。
- 初始化:
sem := make(chan struct{}, maxConcurrency),maxConcurrency是你允许的最大并发数 - 获取许可:
sem (阻塞直到有空位) - 释放许可:
(必须放在 defer 或确保执行的路径上) - 注意:不要用
len(sem)判断剩余容量——它只反映当前已写入未读取的数量,不是“可用数”
用 golang.org/x/sync/semaphore 更安全可靠
官方扩展包 semaphore 提供了带上下文取消、可超时获取、支持权重的生产级实现。比手写 channel 更健壮,尤其适合需要 cancel 或 timeout 的场景。
典型误用是忽略 Acquire 的返回 error:当 context 被 cancel 或超时时,它会返回非 nil error,若不检查会导致逻辑跳过限流直接执行。
- 初始化:
sem := semaphore.NewWeighted(int64(maxConcurrency)) - 获取许可:
if err := sem.Acquire(ctx, 1); err != nil { return err } - 释放许可:
sem.Release(1)(必须与 Acquire 的 weight 匹配) - 权重支持意味着你可以让某些任务占 2 个 slot,某些只占 1 个,适合异构任务混合调度
别在 for 循环里直接起 goroutine + 限流,容易漏 defer
常见模式是在循环中启动大量 goroutine,并对每个加限流。但若把 放在 goroutine 内部又没用 defer 包裹,一旦函数 panic 或提前 return,token 就永远卡住。
正确做法是把 acquire 和 release 拆到 goroutine 外围,或者用闭包+defer 封装干净:
for _, task := range tasks {
sem.Acquire(ctx, 1)
go func(t Task) {
defer sem.Release(1) // 确保释放
doWork(t)
}(task)
}
更稳妥的方式是用 worker pool 模式:固定数量 goroutine 从 channel 消费任务,限流只控制 worker 数量,避免动态启停带来的资源管理混乱。
runtime.GOMAXPROCS 和限流不是一回事
有人混淆 runtime.GOMAXPROCS(控制 OS 线程数)和业务层并发限制。前者影响调度器并行度,后者控制同时运行的任务数。调小 GOMAXPROCS 不会阻止 goroutine 创建,只是让它们排队等线程;而限流是主动拒绝或等待,防止下游被打爆。
比如你设 GOMAXPROCS(2) 但起了 1000 个 goroutine 去调 API,它们仍会密集发起请求,只是 CPU 时间片被轮转得更碎——限流才能真正压住 QPS。
真实系统中,通常两者都要设:用 GOMAXPROCS 合理利用多核,再用 semaphore 控制对外依赖的并发水位。漏掉后者,监控里就容易看到下游 5xx 突增。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











