Go Proxy Scraping: http.Transport, Autenticazione, Rotazione e Concorrenza

In Go, tutta la gestione dei proxy si concentra su un unico campo: Transport.Proxy. Impostalo correttamente e otterrai rotazione per richiesta, SOCKS5 e colly gratuitamente. Impostalo male e avrai 'context canceled' alle 2 del mattino.

Go rende il proxying ingannevolmente semplice: tutta la gestione lato client si concentra su un unico campo, http.Transport.Proxy. Impostalo correttamente e la rotazione per richiesta, SOCKS5 e l'integrazione con colly derivano tutte dallo stesso design. Impostalo male e incontrerai i classici fallimenti dei proxy Go - context canceled, esaurimento del pool di connessioni e una variabile HTTP_PROXY che il tuo trasporto personalizzato ignora silenziosamente. Questa guida percorre l'intero percorso con codice eseguibile.

La soluzione in una riga: http.Transport con un URL proxy

Ogni richiesta proxy in Go passa attraverso un Transport. Il pacchetto net/http fornisce un helper, http.ProxyURL, che fissa un proxy statico per l'intero client. Le credenziali vanno direttamente nell'URL userinfo - Go le trasforma nell'intestazione Proxy-Authorization per te:

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
}

Imposta sempre Client.Timeout. Go non ha un timeout predefinito su http.Client, quindi un singolo exit morto bloccherà il goroutine per sempre - il motivo principale per cui uno scraper Go sembra bloccarsi è un timeout mancante, non un target lento.

Rotazione dei proxy con una funzione Proxy

Proxy non è limitato a un URL fisso - è una funzione func(*http.Request) (*url.URL, error) che Go chiama una volta per richiesta. Questo è il tuo hook di rotazione. Scegli un endpoint casuale da un pool e ogni richiesta esce attraverso un IP diverso, senza logica di gestione della lista nel tuo ciclo di fetch:

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
}

Per la maggior parte degli scraping non dovresti gestire affatto un pool. Punta la funzione Proxy a un singolo gateway residenziale rotante e il gateway ti fornisce un nuovo IP ad ogni richiesta su oltre 90M di indirizzi in oltre 200 paesi - un endpoint, nessuna lista, sessioni sticky quando un flusso necessita dello stesso exit per alcuni minuti.

Diagramma che mostra un Go http.Client che delega una richiesta a http.Transport, che chiama la funzione Proxy per instradare attraverso un gateway residenziale rotante verso il sito target
Un Transport, una funzione Proxy: la funzione viene eseguita per richiesta, quindi un singolo client ruota su tutto il pool.

Autenticazione proxy in Go

Inserire USER:PASS@ nell'URL del proxy è il percorso pulito e funziona sia per ProxyURL che per una funzione Proxy personalizzata. Se preferisci mantenere le credenziali fuori dall'URL, imposta l'intestazione sul tunnel CONNECT da solo tramite Transport.ProxyConnectHeader. Se il tuo provider utilizza l'autenticazione tramite whitelist IP, elimina completamente le credenziali e autorizza l'IP del tuo server nel dashboard - QuantumProxies supporta entrambi. Un CONNECT rifiutato con 407 o proxyconnect tcp significa quasi sempre credenziali mancanti o errate, non un blocco lato target.

La variabile d'ambiente HTTP_PROXY (e quando Go la ignora)

Il http.DefaultTransport di Go utilizza http.ProxyFromEnvironment, che legge HTTP_PROXY, HTTPS_PROXY e NO_PROXY (secondo il pacchetto golang.org/x/net/http/httpproxy). Due comportamenti ingannano le persone. Primo, quelle variabili accettano sia un URL completo che un semplice host:port, dove si presume lo schema http. Secondo, le richieste a localhost o a un indirizzo di loopback bypassano sempre il proxy, restituendo un URL nullo. La più grande insidia è il contrario: nel momento in cui costruisci il tuo &http.Transport{Proxy: ...}, hai sostituito il comportamento delle variabili d'ambiente - la tua funzione esplicita vince e HTTP_PROXY viene ignorato. Se vuoi entrambi, avvolgili tu stesso.

Proxy SOCKS5 in Go

La libreria standard non ha un client SOCKS5, quindi importa golang.org/x/net/proxy e componi tramite esso. Alimenta il dialer in Transport.DialContext in modo che i DNS vengano risolti al proxy, non sulla tua macchina (lo stesso problema di perdita di hostname che socks5h risolve in altri stack). Se stai valutando i due schemi, la nostra nota su SOCKS5 vs HTTP proxies copre quando ciascuno vince:

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}

Proxy in colly

Se fai scraping con colly, ottieni la rotazione senza toccare il transport. Il pacchetto colly/proxy fornisce un RoundRobinProxySwitcher che cicla gli endpoint per richiesta, e LimitRule gestisce il ritmo. Questo è il modo più veloce per un crawler educato e rotante in 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,
})

Concorrenza fatta bene (e la trappola del context-canceled)

La concorrenza di Go è il motivo per cui i team lo scelgono per lo scraping, e anche dove si verifica l'errore context canceled. Si attiva quando il contesto di una richiesta viene annullato prima che il corpo sia completamente letto - di solito un timeout per richiesta scaduto, o un cancel() eseguito troppo presto a causa di un defer posizionato male. Limita il tuo fan-out con un semaforo, dai a ogni richiesta il proprio contesto di timeout e leggi poi chiudi il corpo prima che il cancel venga eseguito:

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()

Altre due note di produzione. Imposta MaxIdleConnsPerHost abbastanza alto affinché un grande pool riutilizzi le connessioni invece di esaurire i descrittori di file - la causa usuale di EOF dopo alcune centinaia di richieste. E ruota l'exit in caso di fallimento piuttosto che riprovare lo stesso; un IP residenziale morto non si riprende al tentativo successivo. Per il design più ampio su milioni di pagine, la nostra guida su large-scale scraping architecture copre code, dedup e tiering dei proxy.

Riutilizza un client, ottimizza il transport

Crea il http.Client una volta e condividilo tra ogni goroutine - è sicuro per l'uso concorrente, e il suo pool di connessioni è ciò che rende veloce uno scraper Go. Un client nuovo per richiesta scarta il keep-alive e forza una nuova stretta di mano TLS ogni volta. Tre campi Transport sono importanti sotto carico: MaxIdleConns e MaxIdleConnsPerHost dimensionano il pool, e IdleConnTimeout ritira le connessioni obsolete in modo che un pool di proxy rotante non fissi mai un exit morto. Impostali esplicitamente - i default della libreria standard sono ottimizzati per un browser, non per un crawler che esegue migliaia di richieste al minuto.

Tabella a due colonne che mappa gli errori proxy Go - proxyconnect rifiutato, context canceled, x509, EOF, HTTP_PROXY ignorato - alle loro cause e soluzioni
Quasi ogni fallimento del proxy Go si mappa su uno di questi cinque - controlla la colonna delle cause prima di riprovare lo stesso exit.

Quando smettere di creare transport a mano

Il codice sopra è sufficiente per target puliti. Una volta che un sito aggiunge Cloudflare, fingerprinting TLS o rendering JavaScript, una stretta di mano TLS grezza di Go non assomiglia affatto a quella di Chrome, e nessun proxy risolve quella discrepanza. A quel punto, un Scraper API che trasporta un vero fingerprint del browser, ruota gli IP e rende JS per te è meno codice e un tasso di successo più alto rispetto a mantenere lo stack a mano. Se stai scegliendo un linguaggio per un nuovo scraper, il nostro post su best proxies for web scraping mappa i compromessi tra gli stack.

Scarica il rendering e la rotazione allo Scraper API

Domande frequenti

Come imposto un proxy per il client HTTP di Go?

Costruisci un http.Transport con un campo Proxy e passalo a http.Client. Usa http.ProxyURL(u) per un singolo proxy statico, o una func(*http.Request) (*url.URL, error) per scegliere l'exit per richiesta. Inserisci le credenziali nell'URL come http://user:pass@host:port e imposta sempre Client.Timeout.

Perché Go ignora la mia variabile HTTP_PROXY?

Perché hai costruito un Transport personalizzato con una funzione Proxy esplicita, che sostituisce il comportamento predefinito ProxyFromEnvironment. Le variabili d'ambiente si applicano solo quando usi http.DefaultTransport o imposti Proxy: http.ProxyFromEnvironment da solo. Nota anche che le richieste a loopback e localhost bypassano sempre il proxy.

Cosa causa 'context canceled' con un proxy Go?

Il contesto della richiesta è stato annullato prima che il corpo della risposta fosse letto - tipicamente un timeout per richiesta scaduto o un cancel() eseguito troppo presto tramite defer. Dai a ogni richiesta il proprio contesto di timeout, svuota e chiudi il corpo prima che il cancel differito venga eseguito, e non condividere un contesto annullabile tra molti goroutine.

Go supporta i proxy SOCKS5?

Non nella libreria standard, ma golang.org/x/net/proxy aggiunge un dialer SOCKS5. Crealo con proxy.SOCKS5 e collegalo a Transport.DialContext in modo che i nomi host vengano risolti al proxy piuttosto che trapelare attraverso una ricerca DNS locale.

Questa è l'intera panoramica lato client: un campo Transport per la configurazione, una funzione per la rotazione, x/net/proxy per SOCKS5, colly per un crawling educato, e un semaforo più contesti per richiesta per la concorrenza. Inizia con un pool rotante pulito e salti la maggior parte della lista degli errori prima che si verifichi.

Inizia con i proxy residenziali di QuantumProxies