用 chan struct{} 实现令牌桶限流本质是控制并发 goroutine 数量,通过带缓冲 channel 作令牌池,缓冲容量即最大并发数;chan struct{} 零内存开销,但必须 make 时指定容量,无缓冲会导致永久阻塞。

用 chan struct{} 做固定容量的并发令牌桶
限流本质是控制同时运行的 goroutine 数量,最轻量的做法就是用带缓冲的 channel 当“令牌池”:每启动一个任务先从 channel 取一个空结构体,任务结束再塞回去。缓冲区大小即最大并发数。
注意:chan struct{} 不占内存,比 chan int 更干净;缓冲容量必须在 make 时确定,运行时不能扩容。
常见错误是把 channel 声明成无缓冲(make(chan struct{})),导致第一个 goroutine 就阻塞——因为没人往里写,它永远取不到令牌。
- 正确写法:
sem := make(chan struct{}, 5)表示最多 5 个并发 - 获取令牌:
sem (阻塞直到有空位) - 释放令牌:
(从 channel 读出一个,腾出一个位置) - 别忘了用
defer func() { 确保异常时也归还令牌
用 time.Ticker + select 实现速率限流(QPS 控制)
如果要限制的是“单位时间请求数”(比如 100 QPS),就不能只靠计数器或 channel 缓冲,得结合时间维度。典型做法是用 time.Ticker 每秒发一次“重置信号”,配合 select 非阻塞尝试获取令牌。
立即学习“go语言免费学习笔记(深入)”;
这种模式适合 API 网关、爬虫请求调度等场景,但要注意:Ticker 的 tick 并不精确,高负载下可能漂移;且它不处理突发流量,需配合漏桶或令牌桶变体。
本文档主要讲述的是android rtsp流媒体播放介绍;实时流协议(RTSP)是应用级协议,控制实时数据的发送。RTSP提供了一个可扩展框架,使实时数据,如音频与视频,的受控、点播成为可能。数据源包括现场数据与存储在剪辑中数据。该协议目的在于控制多个数据发送连接,为选择发送通道,如UDP、组播UDP与TCP,提供途径,并为选择基于RTP上发送机制提供方法。希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
- 维护一个原子计数器(
int64)和sync.RWMutex保护的 lastSecond 字段 - 每次请求进来,用
time.Now().Unix()判断是否跨秒,跨了就重置计数器 - 更稳妥的做法是起一个 goroutine 运行
time.Tick(time.Second),每秒清零计数器并广播 - 避免直接在 handler 里调用
time.Sleep等待,会阻塞整个 goroutine;应改用select+default快速失败
golang.org/x/time/rate 是什么情况下不该用
rate.Limiter 是 Go 官方扩展库提供的成熟限流器,封装了令牌桶逻辑,支持预热、burst、Reserve 等特性。但它不是银弹。
在以下情况建议绕过它,手写更简单的逻辑:
- 只做纯并发数限制(如最多跑 3 个数据库导入任务),用
chan struct{}足够,引入rate反而增加复杂度和锁开销 - 对延迟极度敏感(微秒级响应要求),
rate.Limiter内部有 mutex 和 time.Now 调用,实测比无锁 channel 慢 2–3 倍 - 需要和 context 深度集成(比如 cancel 后立刻清空所有待处理令牌),
rate.Limiter的Wait方法虽支持 context,但无法中断已进入等待队列的请求 - 交叉使用多种限流策略(如“每 IP 每秒 5 次 + 全局并发不超过 10”),组合多个
rate.Limiter容易失控,不如分层 channel 控制
channel 限流容易被忽略的死锁点
用 channel 做限流时,死锁往往不出现在主逻辑,而出现在边界清理环节。
典型场景:goroutine 启动后 panic,没执行 defer 归还令牌;或者主流程提前 return,忘了 close channel 或 drain 剩余令牌;又或者用 for range 读 channel 却没关它,导致 forever blocking。
- 永远在创建 channel 的同一作用域里配对管理:
defer close(sem)不行,因为 channel 要持续复用;应确保每个都有对应sem - 测试时故意让某个 goroutine panic,观察是否卡住——这是检验 defer 归还逻辑是否健壮的最快方式
- 不要用
len(sem) == cap(sem)判断“满”,这只能反映当前占用数,不能替代阻塞语义;真正要判断是否可进,就老实用select+default - 如果限流 channel 是全局变量,注意初始化时机:在
init()里make,别等到第一次调用才 lazy 初始化,否则竞态下可能重复 make









