如何在Golang中实现简单的爬虫限流_Golang channel与time控制方法

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 当“令牌池”。

  • 启动一个 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.DefaultClientTransport.MaxIdleConnsPerHost 默认是 2,高并发下会排队 —— 要调大,否则限流没意义
  • 别把限流逻辑和业务逻辑耦合在同一个 goroutine 里;推荐用“生产者-消费者”模式:一个 goroutine 按节奏发 URL 到任务 channel,另一组 worker 从 channel 消费并执行请求