应避免使用 make(chan struct{}, n) 实现信号量,因其虽编写简便,但存在信号丢失风险。

别用 make(chan struct{}, N) 做信号量
它写起来快,但漏一次 就永久卡死——尤其在 <code>error 分支、panic 或提前 return 时。struct{} 通道没有错误反馈,阻塞后调用方完全无感知,整个池子会慢慢“失血”到无法再进新任务。
- 必须把
放在 goroutine 内部的 <code>defer里,不能靠外层逻辑“记得释放” - 闭包变量(比如
for _, u := range urls { go func() { ... }() })会导致所有 goroutine 共享同一个u,任务错乱 -
len(sem)是已占用数,不是剩余数;真要算可用槽位得写cap(sem) - len(sem),但一般没必要暴露这个 - 缓冲大小固定,无法动态调整,也不支持超时或取消语义
用 golang.org/x/sync/semaphore 替代裸 channel
Go 1.21+ 官方推荐方案,本质是带权重的信号量,天然支持 context 取消和超时,且 panic 后 defer sem.Release(1) 仍会执行,令牌不泄漏。
- 初始化:
sem := semaphore.NewWeighted(16),参数必须是int64,别传0或负数 - 获取许可必须检查 error:
if err := sem.Acquire(ctx, 1); err != nil { return },否则 ctx 超时后任务照常执行 -
sem.Release(1)必须在 goroutine 内部defer,不能放在外层函数里 - 需要快速失败(如限流直接返回 429)时,用
sem.TryAcquire(1),它不阻塞、不看 ctx、不等,成功返回true - 全局单例:不要在每个 handler 里 new 一个,应随 server 生命周期初始化一次
什么时候该升级到 worker pool 而不只是信号量
信号量只回答“现在能跑几个”,但如果你需要控制排队行为、观测指标或熔断 hang 住的任务,就得上 worker pool。
- 任务积压时想主动拒绝(而非让调用方无限等待),需带缓冲的任务队列 + 显式拒绝逻辑
- 要监控当前运行数、排队长度、平均耗时,得暴露
len(pool.tasks)和cap(pool.tasks)等指标 - 某些任务可能长期 hang(比如第三方接口没响应),光靠信号量无法主动中断,必须结合
context.WithTimeout在任务内部控制 - goroutine panic 后需自动 recover 并释放资源,否则 worker 退出,池子实际容量持续缩水
- 推荐
github.com/panjf2000/ants,它预启固定数量 goroutine 持续消费任务队列,支持 panic 捕获、超时、动态伸缩;但别把任务队列设成无缓冲 channel(make(chan func())),那等于没缓冲,一满就阻塞提交方
core.worker_num 和 core.queue_num 怎么设
这两个值不能拍脑袋定,得看负载类型和线上监控反馈。
-
core.worker_num默认为 0(即 CPU 核心数),I/O 密集型任务(如 HTTP 请求)可设为 CPU 数的 2–5 倍;CPU 密集型(如图像压缩)建议 ≤ CPU 核心数 -
core.queue_num是任务缓冲区大小:设太小(如 100)会导致突发流量直接被拒;设太大(如 100w)可能 OOM;推荐从 8192 起步,上线后根据queue_length指标动态调整 - 别把任务队列设成无缓冲 channel,那等于没缓冲,一塞满就阻塞提交方,容易引发级联超时
真正难的不是选哪个库,而是把 sem.Acquire 和 sem.Release 绑定到任务生命周期里——漏掉任意一处,池子就 quietly 开始 leak。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











