Go Proxy Scraping: http.Transport, Autenticação, Rotação e Concorrência
Em Go, toda a história de proxy vive em um campo: Transport.Proxy. Configure corretamente e você terá rotação por solicitação, SOCKS5 e colly gratuitamente. Configure errado e você terá 'context canceled' às 2 da manhã.
Go torna o uso de proxies enganosamente simples: toda a história do lado do cliente vive em um campo, http.Transport.Proxy. Configure corretamente e a rotação por solicitação, SOCKS5 e a integração com colly surgem do mesmo design. Configure errado e você encontrará as falhas clássicas de proxy em Go - context canceled, esgotamento do pool de conexões e uma variável HTTP_PROXY que seu transporte personalizado ignora silenciosamente. Este guia percorre todo o caminho com código executável.
A linha única: http.Transport com uma URL de proxy
Toda solicitação com proxy em Go passa por um Transport. O pacote net/http oferece um auxiliar, http.ProxyURL, que fixa um proxy estático para todo o cliente. As credenciais vão direto para o userinfo da URL - Go as transforma no cabeçalho Proxy-Authorization para você:
package main
import (
"fmt"
"net/http"
"net/url"
"time"
)
func main() {
proxyURL, _ := url.Parse("http://USER:PASS@gate.quantumproxies.io:8000")
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
}
Sempre configure Client.Timeout. Go não tem tempo limite padrão em http.Client, então uma única saída inativa bloqueará a goroutine para sempre - a principal razão pela qual um scraper Go parece travar é a falta de um tempo limite, não um alvo lento.
Rotação de proxies com uma função Proxy
Proxy não é limitado a uma URL fixa - é uma função func(*http.Request) (*url.URL, error) que Go chama uma vez por solicitação. Esse é o seu gancho de rotação. Escolha um endpoint aleatório de um pool e cada solicitação sai por um IP diferente, sem lógica de gerenciamento de lista no seu loop de busca:
import "math/rand"
pool := []string{
"http://USER:PASS@ip1.quantumproxies.io:8000",
"http://USER:PASS@ip2.quantumproxies.io:8000",
"http://USER:PASS@ip3.quantumproxies.io:8000",
}
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
}
Para a maioria das raspagens, você não deve gerenciar um pool. Aponte a função Proxy para um único gateway residencial rotativo e o gateway lhe fornece um IP novo em cada solicitação em mais de 90 milhões de endereços em mais de 200 países - um endpoint, sem lista, sessões persistentes quando um fluxo precisa da mesma saída por alguns minutos.

Autenticação de Proxy em Go
Colocar USER:PASS@ na URL do proxy é o caminho limpo e funciona tanto para ProxyURL quanto para uma função Proxy personalizada. Se preferir manter as credenciais fora da URL, defina o cabeçalho no túnel CONNECT você mesmo via Transport.ProxyConnectHeader. Se seu provedor usar autenticação por lista branca de IP, elimine completamente as credenciais e autorize o IP do seu servidor no painel - QuantumProxies suporta ambos. Um CONNECT recusado com 407 ou proxyconnect tcp quase sempre significa credenciais ausentes ou incorretas, não um bloqueio do lado do alvo.
A variável de ambiente HTTP_PROXY (e quando Go a ignora)
O http.DefaultTransport do Go usa http.ProxyFromEnvironment, que lê HTTP_PROXY, HTTPS_PROXY e NO_PROXY (conforme o pacote golang.org/x/net/http/httpproxy). Dois comportamentos confundem as pessoas. Primeiro, essas variáveis aceitam uma URL completa ou um host:port simples, onde o esquema http é assumido. Segundo, solicitações para localhost ou um endereço de loopback sempre ignoram o proxy, retornando uma URL nula. O maior problema é o inverso: no momento em que você constrói seu próprio &http.Transport{Proxy: ...}, você substituiu o comportamento da variável de ambiente - sua função explícita vence e HTTP_PROXY é ignorado. Se você quiser ambos, envolva-os você mesmo.
Proxies SOCKS5 em Go
A biblioteca padrão não tem um cliente SOCKS5, então use golang.org/x/net/proxy e faça a discagem através dele. Alimente o dialer em Transport.DialContext para que o DNS seja resolvido no proxy, não na sua máquina (o mesmo problema de vazamento de nome de host que socks5h resolve em outras pilhas). Se você está pesando os dois esquemas, nossa nota sobre SOCKS5 vs HTTP proxies cobre quando cada um vence:
go get golang.org/x/net/proxy
import (
"context"
"net"
"net/http"
"golang.org/x/net/proxy"
)
auth := &proxy.Auth{User: "USER", Password: "PASS"}
dialer, err := proxy.SOCKS5("tcp", "gate.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}
Proxies em colly
Se você raspa com colly, você obtém rotação sem tocar no transporte. O pacote colly/proxy oferece um RoundRobinProxySwitcher que alterna endpoints por solicitação, e LimitRule lida com o ritmo. Esta é a maneira mais rápida de um crawler rotativo e educado em 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:PASS@ip1.quantumproxies.io:8000",
"http://USER:PASS@ip2.quantumproxies.io:8000",
)
if err != nil {
panic(err)
}
c.SetProxyFunc(rp)
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 8,
RandomDelay: 2 * time.Second,
})
Concorrência feita corretamente (e a armadilha do context-canceled)
A concorrência do Go é a razão pela qual as equipes o escolhem para raspagem, e também de onde vem o erro context canceled. Ele dispara quando o contexto de uma solicitação é cancelado antes que o corpo seja totalmente lido - geralmente um tempo limite por solicitação que expirou, ou um cancel() que foi executado muito cedo por causa de um defer mal colocado. Limite sua fan-out com um semáforo, dê a cada solicitação seu próprio contexto de tempo limite, e leia e feche o corpo antes que o cancelamento dispare:
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()
Mais duas notas de produção. Defina MaxIdleConnsPerHost alto o suficiente para que um grande pool reutilize conexões em vez de esgotar descritores de arquivo - a causa usual de EOF após algumas centenas de solicitações. E rode a saída em caso de falha em vez de tentar novamente a mesma; um IP residencial inativo não se recupera na próxima tentativa. Para o design mais amplo em milhões de páginas, nosso guia sobre arquitetura de raspagem em larga escala cobre filas, deduplicação e nivelamento de proxy.
Reutilize um cliente, ajuste o transporte
Crie o http.Client uma vez e compartilhe-o em cada goroutine - é seguro para uso concorrente, e seu pool de conexões é o que torna um scraper Go rápido. Um cliente novo por solicitação descarta o keep-alive e força um novo handshake TLS toda vez. Três campos Transport importam sob carga: MaxIdleConns e MaxIdleConnsPerHost dimensionam o pool, e IdleConnTimeout aposenta conexões inativas para que um pool de proxy rotativo nunca fixe uma saída inativa. Defina-os explicitamente - os padrões da biblioteca padrão são ajustados para um navegador, não um crawler executando milhares de solicitações por minuto.

Quando parar de criar transports manualmente
O código acima é suficiente para alvos limpos. Uma vez que um site adiciona Cloudflare, impressão digital TLS ou renderização de JavaScript, um handshake TLS bruto do Go não se parece em nada com o do Chrome, e nenhum proxy corrige essa incompatibilidade. Nesse ponto, uma Scraper API que carrega uma impressão digital de navegador real, roda IPs e renderiza JS para você é menos código e uma taxa de sucesso mais alta do que manter a pilha manualmente. Se você está escolhendo uma linguagem para um novo scraper, nosso post sobre melhores proxies para raspagem web mapeia as trocas entre pilhas.
Descarregue a renderização e rotação para a Scraper API
Perguntas frequentes
Como configuro um proxy para o cliente HTTP do Go?
Construa um http.Transport com um campo Proxy e passe-o para http.Client. Use http.ProxyURL(u) para um único proxy estático, ou uma func(*http.Request) (*url.URL, error) para escolher a saída por solicitação. Coloque as credenciais na URL como http://user:pass@host:port e sempre defina Client.Timeout.
Por que Go ignora minha variável HTTP_PROXY?
Porque você construiu um Transport personalizado com uma função Proxy explícita, que substitui o comportamento padrão ProxyFromEnvironment. As variáveis de ambiente só se aplicam quando você usa http.DefaultTransport ou define Proxy: http.ProxyFromEnvironment você mesmo. Além disso, observe que solicitações para loopback e localhost sempre ignoram o proxy.
O que causa 'context canceled' com um proxy Go?
O contexto da solicitação foi cancelado antes que o corpo da resposta fosse lido - tipicamente um tempo limite por solicitação expirado ou um cancel() que foi executado muito cedo via defer. Dê a cada solicitação seu próprio contexto de tempo limite, esgote e feche o corpo antes que o cancelamento adiado seja executado, e não compartilhe um único contexto cancelável entre muitas goroutines.
Go suporta proxies SOCKS5?
Não na biblioteca padrão, mas golang.org/x/net/proxy adiciona um dialer SOCKS5. Crie-o com proxy.SOCKS5 e conecte-o em Transport.DialContext para que nomes de host sejam resolvidos no proxy em vez de vazar através de uma consulta DNS local.
Essa é toda a imagem do lado do cliente: um campo Transport para configuração, uma função para rotação, x/net/proxy para SOCKS5, colly para raspagem educada, e um semáforo mais contextos por solicitação para concorrência. Comece com um pool rotativo limpo e você evita a maioria da lista de erros antes que eles aconteçam.