Go中限流最轻量可控方式是time.Ticker配合channel;固定频率限流用Ticker实现QPS控制,需注意信号积压问题;令牌桶限流则用带缓冲chan模拟burst和rate。

Go 里实现爬虫限流,最轻量、最可控的方式就是用 time.Ticker 配合 channel 控制请求节奏,而不是依赖第三方库或复杂调度器。
用 time.Ticker 做固定频率限流
这是最直观的限流方式:每 N 毫秒放行一个请求。适用于目标站点允许稳定 QPS(比如 10 QPS → 每 100ms 一个请求)。
-
Ticker是持续发送时间信号的 channel,比反复time.Sleep更精确、更易管理 - 必须在 goroutine 中消费
Ticker.C,否则会阻塞;也别忘了defer ticker.Stop() - 若请求处理耗时超过 tick 间隔,
Ticker会积压信号,导致“脉冲式”并发 —— 这不是你想要的限流,得加缓冲或改用time.AfterFunc方式
func main() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
urls := []string{"https://example.com/1", "https://example.com/2", ...}
for _, url := range urls {
<-ticker.C // 等待下一个时间点
go func(u string) {
resp, err := http.Get(u)
if err != nil {
log.Printf("failed to fetch %s: %v", u, err)
return
}
defer resp.Body.Close()
}(url)
}
// 注意:这里没等 goroutine 结束,实际需用 sync.WaitGroup}
用带缓冲的 chan struct{} 实现令牌桶式限流
当需要支持突发流量(比如允许最多 5 个请求瞬间发出,之后限速为 10 QPS),纯 Ticker 不够用,得模拟令牌桶。核心是用带缓冲的 channel 当“令牌池”。
立即学习“go语言免费学习笔记(深入)”;
- 启动一个 goroutine 持续往
tokenCh里塞令牌(struct{}占 0 字节,最省) - 每次发请求前从
tokenCh取一个令牌 —— 若缓冲已满,就阻塞等待;若取到就继续 - 缓冲容量 = 最大并发数(burst),填充速率 = 1 / 间隔(rate)
- 注意:不要用
len(tokenCh)判断剩余令牌,因为并发下不准确;channel 本身已提供线程安全的计数语义
func newTokenBucket(burst int, rate time.Duration) <-chan struct{} {
ch := make(chan struct{}, burst)
go func() {
ticker := time.NewTicker(rate)
defer ticker.Stop()
for range ticker.C {
select {
case ch <- struct{}{}:
default:
}
}
}()
return ch
}
func main() {
tokenCh := newTokenBucket(5, 100*time.Millisecond) // 允许最多 5 个并发,平均 10 QPS
for _, url := range urls {
<-tokenCh // 拿令牌,阻塞直到有空位
go func(u string) {
defer func() { <-tokenCh }() // 请求结束归还?不,这里是单向发放,不回收
http.Get(u)
}(url)
}}
为什么不用 time.Sleep 直接控制?
看似简单,但容易出错:
- 在循环里写
time.Sleep(100 * time.Millisecond),如果某次http.Get耗时 500ms,那下一次请求就在 600ms 后才发 —— 实际 QPS 远低于预期 - 无法应对失败重试:重试逻辑若插在 sleep 前后,会打乱节奏;插在中间又可能让重试挤占正常请求 slot
- sleep 是 goroutine 级阻塞,而
Ticker+ channel 是协作式控制,更利于组合(比如和context.WithTimeout一起用)
真实场景中容易忽略的关键点
限流只是爬虫健壮性的一环,真正上线时这几个细节常被跳过:
- HTTP client 必须设
Timeout,否则一个卡住的请求会让整个限流 channel 堵死 - 域名级限流比全局限流更重要:对不同 host 使用独立的
tokenCh,避免 A 站慢拖垮 B 站 -
http.DefaultClient的Transport.MaxIdleConnsPerHost默认是 2,高并发下会排队 —— 要调大,否则限流没意义 - 别把限流逻辑和业务逻辑耦合在同一个 goroutine 里;推荐用“生产者-消费者”模式:一个 goroutine 按节奏发 URL 到任务 channel,另一组 worker 从 channel 消费并执行请求










