# Go Proxy Scraping: http.Transport, аутентификация, ротация и параллелизм

В Go вся история с прокси сосредоточена в одном поле: Transport.Proxy. Настройте его правильно, и вы получите ротацию для каждого запроса, SOCKS5 и colly бесплатно. Настройте неправильно, и вы получите 'context canceled' в 2 часа ночи.

Go делает проксирование обманчиво простым: вся клиентская история сосредоточена в одном поле, `http.Transport.Proxy`. Настройте его правильно, и ротация для каждого запроса, SOCKS5 и интеграция с `colly` будут работать по одному и тому же принципу. Настройте неправильно, и вы столкнетесь с классическими ошибками Go прокси - `context canceled`, истощением пула соединений и переменной `HTTP_PROXY`, которую ваш пользовательский транспорт тихо игнорирует. Это руководство проведет вас по всему пути с исполняемым кодом.

## Однострочник: http.Transport с URL прокси

Каждый запрос через прокси в Go проходит через `Transport`. Пакет `net/http` содержит помощник, `http.ProxyURL`, который закрепляет один статический прокси для всего клиента. Учетные данные вставляются прямо в URL userinfo - Go преобразует их в заголовок `Proxy-Authorization` за вас:

```go
package main

import (
  "fmt"
  "net/http"
  "net/url"
  "time"
)

func main() {
  proxyURL, _ := url.Parse("http://USER:PASS@residentialpremium.quantumproxies.io:8080")
  client := &http.Client{
    Timeout:   20 * time.Second,
    Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
  }
  resp, err := client.Get("https://httpbin.org/ip")
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()
  fmt.Println(resp.Status) // body shows the exit IP, not yours
}
```

Всегда устанавливайте `Client.Timeout`. В Go по умолчанию нет тайм-аута на `http.Client`, поэтому один зависший выход заблокирует горутину навсегда - главная причина, по которой Go-скрапер кажется зависшим, это отсутствие тайм-аута, а не медленная цель.

## Ротация прокси с помощью функции Proxy

`Proxy` не ограничивается фиксированным URL - это функция `func(*http.Request) (*url.URL, error)`, которую Go вызывает один раз на каждый запрос. Это ваш крючок для ротации. Выберите случайную конечную точку из пула, и каждый запрос будет выходить через другой IP, без какой-либо логики управления списком в вашем цикле выборки:

```go
import "math/rand"

// three sticky sessions on the same gateway = three exit IPs to pick from
pool := []string{
  "http://USER-sid-pool1-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
  "http://USER-sid-pool2-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
  "http://USER-sid-pool3-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
}

transport := &http.Transport{
  Proxy: func(r *http.Request) (*url.URL, error) {
    return url.Parse(pool[rand.Intn(len(pool))])
  },
  MaxIdleConnsPerHost: 32, // reuse connections across the pool
}
```

Для большинства скрапинга вам не следует управлять пулом вообще. Направьте функцию `Proxy` на один [ротационный резидентный шлюз](https://quantumproxies.io/rotating-proxies), и шлюз предоставит вам новый IP на каждый запрос из более чем 90 миллионов адресов в более чем 200 странах - одна конечная точка, без списка, липкие сессии, когда поток требует одного выхода на несколько минут.

![Один http.Client в Go с одним Transport, чья функция Proxy выполняется для каждого запроса, поэтому запросы 1, 2 и 3 уходят к одной и той же цели через разные выходные IP. Карточка: функция вызывается один раз на запрос, выбирает из пула или указывает на один ротационный шлюз, а MaxIdleConnsPerHost: 32 обеспечивает повторное использование соединений.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335148/blog/generated/qp-b9-009-diag1.png)
*Один Transport, одна функция Proxy: функция выполняется для каждого запроса, так что один клиент вращается по всему пулу.*

## Аутентификация прокси в Go

Вставка `USER:PASS@` в URL прокси - это чистый путь и работает как для `ProxyURL`, так и для пользовательской функции `Proxy`. Если вы предпочитаете не включать учетные данные в URL, установите заголовок на туннеле CONNECT самостоятельно через `Transport.ProxyConnectHeader`. Если ваш провайдер использует аутентификацию по белому списку IP, полностью откажитесь от учетных данных и авторизуйте IP вашего сервера в панели управления - QuantumProxies поддерживает оба варианта. Отказ в CONNECT с `407` или `proxyconnect tcp` почти всегда означает отсутствие или неправильные учетные данные, а не блокировку на стороне цели.

## Переменная окружения HTTP_PROXY (и когда Go её игнорирует)

Go's `http.DefaultTransport` использует `http.ProxyFromEnvironment`, который читает `HTTP_PROXY`, `HTTPS_PROXY` и `NO_PROXY` (в соответствии с пакетом `golang.org/x/net/http/httpproxy`). Два поведения сбивают людей с толку. Во-первых, эти переменные принимают либо полный URL, либо голый `host:port`, где предполагается схема `http`. Во-вторых, запросы к `localhost` или адресу обратной петли всегда обходят прокси, возвращая nil URL. Большая ловушка - это обратное: как только вы создаете свой собственный `&http.Transport{Proxy: ...}`, вы *заменяете* поведение переменной окружения - ваша явная функция побеждает, и `HTTP_PROXY` игнорируется. Если вы хотите и то, и другое, оберните их самостоятельно.

## SOCKS5 прокси в Go

Стандартная библиотека не имеет клиента SOCKS5, поэтому подключите `golang.org/x/net/proxy` и настраивайте через него. Подключите дайлер в `Transport.DialContext`, чтобы DNS разрешался на прокси, а не на вашем компьютере (та же проблема утечки имени хоста, которую решает `socks5h` в других стэках). Если вы взвешиваете две схемы, наша заметка о [SOCKS5 vs HTTP прокси](https://quantumproxies.io/blog/socks5-vs-http-proxy-difference) объясняет, когда каждая из них выигрывает:

```bash
go get golang.org/x/net/proxy
```

```go
import (
  "context"
  "net"
  "net/http"
  "golang.org/x/net/proxy"
)

auth := &proxy.Auth{User: "USER", Password: "PASS"}
dialer, err := proxy.SOCKS5("tcp", "residentialpremium.quantumproxies.io:1080", auth, proxy.Direct)
if err != nil {
  panic(err)
}

transport := &http.Transport{
  DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
    return dialer.Dial(network, addr)
  },
}
client := &http.Client{Transport: transport}
```

## Прокси в colly

Если вы скрапите с `colly`, вы получаете ротацию, не касаясь транспорта. Пакет `colly/proxy` поставляется с `RoundRobinProxySwitcher`, который циклически меняет конечные точки для каждого запроса, а `LimitRule` управляет темпом. Это самый быстрый способ создать вежливый, ротационный скрапер в Go:

```go
import (
  "time"
  "github.com/gocolly/colly/v2"
  "github.com/gocolly/colly/v2/proxy"
)

c := colly.NewCollector(colly.Async(true))

rp, err := proxy.RoundRobinProxySwitcher(
  "http://USER-sid-crawl1-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
  "http://USER-sid-crawl2-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080",
)
if err != nil {
  panic(err)
}
c.SetProxyFunc(rp)

c.Limit(&colly.LimitRule{
  DomainGlob:  "*",
  Parallelism: 8,
  RandomDelay: 2 * time.Second,
})
```

## Параллелизм сделан правильно (и ловушка context-canceled)

Параллелизм Go - это причина, по которой команды выбирают его для скрапинга, а также источник ошибки `context canceled`. Она возникает, когда контекст запроса отменяется до того, как тело полностью прочитано - обычно это истекший тайм-аут для каждого запроса или `cancel()`, который сработал слишком рано из-за неправильно расположенного `defer`. Ограничьте ваш фан-аут семафором, дайте каждому запросу свой собственный контекст тайм-аута и читайте, а затем закрывайте тело до того, как сработает отмена:

```go
sem := make(chan struct{}, 20) // cap concurrency
var wg sync.WaitGroup

for _, u := range urls {
  wg.Add(1)
  sem <- struct{}{}
  go func(u string) {
    defer wg.Done()
    defer func() { <-sem }()

    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
    resp, err := client.Do(req)
    if err != nil {
      return // rotate/log; do not retry the same burned exit
    }
    io.Copy(io.Discard, resp.Body) // drain BEFORE cancel fires
    resp.Body.Close()
  }(u)
}
wg.Wait()
```

Еще две заметки для производства. Установите `MaxIdleConnsPerHost` достаточно высоким, чтобы большой пул переиспользовал соединения, а не истощал файловые дескрипторы - обычная причина `EOF` после нескольких сотен запросов. И вращайте выход при сбое, а не повторяйте попытку с тем же; мертвый резидентный IP не восстанавливается при следующей попытке. Для более широкого дизайна на миллионах страниц наше руководство по [архитектуре крупномасштабного скрапинга](https://quantumproxies.io/blog/large-scale-web-scraping-architecture) охватывает очереди, дедупликацию и уровни прокси.

## Переиспользуйте одного клиента, настройте транспорт

Создайте `http.Client` один раз и используйте его во всех горутинах - он безопасен для параллельного использования, и его пул соединений делает Go-скрапер быстрым. Новый клиент для каждого запроса выбрасывает keep-alive и вынуждает каждый раз выполнять новое TLS рукопожатие. Три поля `Transport` важны под нагрузкой: `MaxIdleConns` и `MaxIdleConnsPerHost` задают размер пула, а `IdleConnTimeout` завершает устаревшие соединения, чтобы ротационный пул прокси никогда не закреплял мертвый выход. Установите их явно - стандартные настройки библиотеки настроены для браузера, а не для скрапера, выполняющего тысячи запросов в минуту.

![Таблица ошибок прокси в Go с выделенной колонкой причин. Карточка сопоставляет каждую ошибку с причиной: proxyconnect или 407 означают неверные учетные данные, context canceled означает, что cancel() выполнился до чтения тела, EOF означает слишком низкий MaxIdleConnsPerHost, игнорируемый HTTP_PROXY означает собственный Transport; зависание означает отсутствие таймаута.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335148/blog/generated/qp-b9-009-diag2.png)
*Почти каждая ошибка Go прокси сопоставляется с одной из этих пяти - проверьте столбец причин, прежде чем повторять попытку с тем же выходом.*

## Когда стоит прекратить ручную настройку транспортов

Код выше достаточен для чистых целей. Как только сайт добавляет Cloudflare, TLS отпечатки или рендеринг JavaScript, чистое TLS рукопожатие Go не похоже на Chrome, и никакой прокси не исправит это несоответствие. В этот момент [Scraper API](https://quantumproxies.io/scraper-api), который использует настоящий отпечаток браузера, вращает IP и рендерит JS за вас, будет менее кода и более высокой успешностью, чем поддержание стека вручную. Если вы выбираете язык для нового скрапера, наш пост о [лучших прокси для веб-скрапинга](https://quantumproxies.io/blog/best-proxies-for-web-scraping) описывает компромиссы между стеками.

[Перенесите рендеринг и ротацию на Scraper API](https://quantumproxies.io/scraper-api)

## Часто задаваемые вопросы

### Как установить прокси для Go HTTP клиента?

Создайте `http.Transport` с полем `Proxy` и передайте его в `http.Client`. Используйте `http.ProxyURL(u)` для одного статического прокси или `func(*http.Request) (*url.URL, error)`, чтобы выбрать выход для каждого запроса. Вставьте учетные данные в URL как `http://user:pass@host:port` и всегда устанавливайте `Client.Timeout`.

### Почему Go игнорирует мою переменную HTTP_PROXY?

Потому что вы создали пользовательский `Transport` с явной функцией `Proxy`, которая заменяет поведение по умолчанию `ProxyFromEnvironment`. Переменные окружения применяются только когда вы используете `http.DefaultTransport` или устанавливаете `Proxy: http.ProxyFromEnvironment` самостоятельно. Также учтите, что запросы на loopback и localhost всегда обходят прокси.

### Что вызывает 'context canceled' с Go прокси?

Контекст запроса был отменен до того, как тело ответа было прочитано - обычно это истекший тайм-аут для каждого запроса или `cancel()`, который сработал слишком рано через `defer`. Дайте каждому запросу свой собственный контекст тайм-аута, слейте и закройте тело до того, как выполнится отложенная отмена, и не делитесь одним отменяемым контекстом между многими горутинами.

### Поддерживает ли Go SOCKS5 прокси?

Не в стандартной библиотеке, но `golang.org/x/net/proxy` добавляет дайлер SOCKS5. Создайте его с `proxy.SOCKS5` и подключите в `Transport.DialContext`, чтобы имена хостов разрешались на прокси, а не утекали через локальный DNS-запрос.

Это вся картина на стороне клиента: одно поле `Transport` для настройки, функция для ротации, `x/net/proxy` для SOCKS5, `colly` для вежливого скрапинга и семафор плюс контексты для каждого запроса для параллелизма. Начните с чистого ротационного пула, и вы избежите большинства ошибок до их появления.

[Начните с резидентных прокси QuantumProxies](https://quantumproxies.io/residential-proxies)
