应优先使用golang.org/x/sync/semaphore而非chan struct{}:前者官方推荐、支持上下文取消和超时、panic安全;后者仅适用于无取消、无超时、固定权重的简单场景,如cli工具或离线批处理。

Go 语言没有内置 semaphore 类型,所谓“通过语言学习掌握”不是靠背概念,而是立刻分辨出:该用 chan struct{} 还是 golang.org/x/sync/semaphore —— 选错就踩坑。
什么时候直接用 make(chan struct{}, N)
适合启动脚本、CLI 工具、离线批处理等**无上下文取消、无超时、固定权重(每次占 1 单位)** 的场景。
- 初始化:
sem := make(chan struct{}, 5),容量就是最大并发数,别写成make(chan struct{})(无缓冲 = 串行)或make(chan int, 5)(语义错,int 值无意义) - 获取:
sem —— 必须是发送操作,不是接收;别漏掉 <code>{},struct{}是零大小类型,语义唯一 - 释放:
—— 必须是接收操作,且只能在成功 acquire 后调用;永远不要 <code>close(sem),会 panic - 防漏写:用
defer包裹释放逻辑,但确保只在 acquire 成功后才 defer,否则可能 panic(如recv on closed channel)
Acquire(ctx, n) 返回 error 时为什么不能调用 Release(n)
semaphore.Weighted.Acquire 不是“拿资源”,而是“租约申请”。返回非 nil error(比如 context.Canceled 或 context.DeadlineExceeded)意味着你根本没拿到许可,计数器没变,状态没动。
- 错误写法:
if err := sem.Acquire(ctx, 1); err != nil { log.Println(err); sem.Release(1) }—— 这会破坏内部原子计数器,后续所有调用可能永久阻塞 - 正确模式:只有
err == nil时才 defersem.Release(1);Release不接受 context,也不检查是否“持有”,它只做减法 - 常见翻车点:把
Acquire放在defer前面,但函数中间 panic 或 return,导致Release被执行了两次(一次 defer,一次手动),计数器溢出
TryAcquire(n) 不是“轻量版 Acquire”,而是完全不同的控制流
TryAcquire 立刻返回 bool,不看 context,也不阻塞。它适用于**不想等、愿意退避或丢弃**的场景,比如后台任务调度、重试队列、健康检查探针。
- HTTP handler 里别用
TryAcquire—— 客户端在等响应,你返回 false 就等于静默失败,必须用Acquire(ctx, 1)配合 timeout - 后台 worker 中可用:
if sem.TryAcquire(1) { defer sem.Release(1); doWork() },失败就跳过,不阻塞主循环 - 别写成:
for !sem.TryAcquire(1) { time.Sleep(10ms) }—— 这是忙等,浪费 CPU,违背 Go channel 调度哲学
权重参数 n 在真实流量控制中多数时候是陷阱
Acquire(ctx, 3) 看起来灵活,但在 HTTP 或 RPC 场景中,它会让监控、限流策略和 debug 变得混乱:一个请求占 3 单位,另一个占 1,CurrentCount() 返回的是总权重,不是活跃请求数。
- 除非你明确区分“大任务”和“小任务”,且有配套的路由/打标/统计能力,否则一律用
Acquire(ctx, 1) -
semaphore.NewWeighted(10)的 10 是最大权重总和,不是最大 goroutine 数 —— 如果全用Acquire(ctx, 2),实际最多 5 个并发,不是 10 个 - 权重拆分释放(如
Acquire(ctx, 3)后调三次Release(1))不会 panic,但sem.CurrentCount()会失真,无法对齐业务指标
最易被忽略的点:信号量不管理“谁持有”,只维护一个整数。一旦 Acquire 和 Release 配对出错,状态就不可逆 —— 没有 reset 方法,也没有 debug 接口,只能重启服务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











