正确做法是用 time.ticker 在外部驱动 goroutine 启动,确保最小间隔;错误做法是在 goroutine 内 sleep,导致并发爆发。如 tick := time.newticker(100 * time.millisecond),在 for-select 中 case

用 time.Ticker 控制 goroutine 启动节奏
直接在 goroutine 内部 sleep 无法真正限频——它只控制执行时长,不约束启动间隔。正确做法是用 time.Ticker 在外部驱动,确保每次启动至少间隔指定时间。
常见错误是把 time.Sleep 放在 goroutine 开头,结果并发量一上来,大量 goroutine 同时被唤醒,瞬间打满下游。
- 创建
time.Ticker:例如tick := time.NewTicker(100 * time.Millisecond) - 在 for-select 循环中接收 tick:
case ,再 spawn goroutine 或调用处理函数 - 务必在程序退出前调用
tick.Stop(),否则 ticker 会泄漏 goroutine 和 timer 资源
用 rate.Limiter 实现带桶机制的请求限频
当需要更精细控制(比如每秒最多 10 次、允许突发 3 次),golang.org/x/time/rate 的 rate.Limiter 是标准解法。它基于 token bucket,支持阻塞等待或非阻塞试探。
注意:它限制的是「调用频率」,不是 goroutine 数量;你仍需自己决定是否启动新 goroutine —— 通常建议先 limiter.Wait(ctx) 成功后再启。
-
limiter := rate.NewLimiter(10, 3)表示平均 10 QPS,最多积压 3 个 token - 阻塞式:用
limiter.Wait(ctx),超时或取消会返回 error - 非阻塞式:用
limiter.Allow()返回 bool,适合快速失败场景 - 别在循环里反复新建
rate.Limiter,它是线程安全的,应复用单例
goroutine 泄漏风险:别让限频逻辑卡死协程
限频本身不该成为 goroutine 停止的唯一条件。如果下游服务挂了,而你的 goroutine 只等 ticker 或 limiter,它就永远卡着,越积越多。
典型表现是 goroutine 数持续上涨,pprof 查看堆栈停在 runtime.gopark 或 select 上。
- 所有等待操作必须绑定
context.Context,尤其是limiter.Wait(ctx)和time.AfterFunc类型调用 - 避免在 goroutine 内无条件
for { select { case ,没退出路径 - 若用 channel 控制启停,确保发送方 close channel 后,接收方能感知并退出
要不要用带缓冲 channel 做“频率队列”?
有人用固定长度 channel(如 make(chan struct{}, 5))做信号量,控制并发数,但这不是频率控制——它只限数量,不限单位时间次数。5 个 goroutine 可能在 1ms 内全冲出去。
如果你真需要“最多同时运行 N 个 + 每个间隔 M 时间”,得组合使用:semaphore 控并发数 + rate.Limiter 控节奏,且顺序不能错:先过限频,再抢信号量。
- 错误顺序:
sem → 可能多个 goroutine 已抢占 sem,再一起等 limiter 放行 - 正确顺序:
limiter.Wait(ctx); sem → 频率先控住,再进并发池 - 缓冲 channel 本身不解决时间维度问题,别把它当 ticker 用
实际限频逻辑往往夹在业务路径中间,最容易被忽略的是 context 生命周期和 limiter 复用粒度——一个 HTTP handler 里 new 一个 limiter,等于每请求建一个桶,完全失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











