Go Proxy Scraping: http.Transport, Autenticación, Rotación y Concurrencia
En Go, toda la historia de los proxies se centra en un campo: Transport.Proxy. Configúralo correctamente y obtienes rotación por solicitud, SOCKS5 y colly gratis. Configúralo incorrectamente y obtienes 'context canceled' a las 2am.
Go hace que el uso de proxies sea engañosamente simple: toda la historia del lado del cliente se centra en un solo campo, http.Transport.Proxy. Configúralo bien y la rotación por solicitud, SOCKS5 y la integración con colly se derivan del mismo diseño. Configúralo mal y te encontrarás con los fallos clásicos de proxy en Go: context canceled, agotamiento del pool de conexiones, y una variable HTTP_PROXY que tu transporte personalizado ignora silenciosamente. Esta guía recorre todo el camino con código ejecutable.
La línea de una sola vez: http.Transport con una URL de proxy
Cada solicitud proxy en Go fluye a través de un Transport. El paquete net/http incluye un ayudante, http.ProxyURL, que fija un proxy estático para todo el cliente. Las credenciales van directamente en la información de usuario de la URL - Go las convierte en el encabezado Proxy-Authorization por ti:
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
}
Siempre establece Client.Timeout. Go no tiene un tiempo de espera predeterminado en http.Client, por lo que una única salida muerta bloqueará la goroutine para siempre - la razón número uno por la que un scraper en Go parece colgarse es un tiempo de espera faltante, no un objetivo lento.
Rotación de proxies con una función Proxy
Proxy no se limita a una URL fija - es una función func(*http.Request) (*url.URL, error) que Go llama una vez por solicitud. Esa es tu conexión de rotación. Elige un endpoint aleatorio de un pool y cada solicitud sale a través de una IP diferente, sin lógica de gestión de listas en tu bucle de obtención:
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 la mayoría de los scraping no deberías gestionar un pool en absoluto. Apunta la función Proxy a un único gateway residencial rotativo y el gateway te proporciona una IP nueva en cada solicitud a través de más de 90 millones de direcciones en más de 200 países - un endpoint, sin lista, sesiones pegajosas cuando un flujo necesita la misma salida por unos minutos.

Autenticación de proxy en Go
Poner USER:PASS@ en la URL del proxy es el camino limpio y funciona tanto para ProxyURL como para una función Proxy personalizada. Si prefieres mantener las credenciales fuera de la URL, establece el encabezado en el túnel CONNECT tú mismo a través de Transport.ProxyConnectHeader. Si tu proveedor utiliza autenticación por lista blanca de IP, elimina las credenciales por completo y autoriza la IP de tu servidor en el panel de control - QuantumProxies admite ambos. Un CONNECT rechazado con 407 o proxyconnect tcp casi siempre significa credenciales faltantes o incorrectas, no un bloqueo del lado del objetivo.
La variable de entorno HTTP_PROXY (y cuando Go la ignora)
El http.DefaultTransport de Go utiliza http.ProxyFromEnvironment, que lee HTTP_PROXY, HTTPS_PROXY y NO_PROXY (según el paquete golang.org/x/net/http/httpproxy). Dos comportamientos confunden a la gente. Primero, esas variables aceptan una URL completa o un simple host:port, donde se asume el esquema http. Segundo, las solicitudes a localhost o una dirección de loopback siempre pasan por alto el proxy, devolviendo una URL nula. La mayor trampa es la inversa: en el momento en que construyes tu propio &http.Transport{Proxy: ...}, has reemplazado el comportamiento de la variable de entorno - tu función explícita gana y HTTP_PROXY es ignorado. Si quieres ambos, envuélvelos tú mismo.
Proxies SOCKS5 en Go
La biblioteca estándar no tiene un cliente SOCKS5, así que incorpora golang.org/x/net/proxy y marca a través de él. Alimenta el dialer en Transport.DialContext para que las DNS se resuelvan en el proxy, no en tu máquina (el mismo problema de fuga de nombres de host que socks5h resuelve en otros stacks). Si estás sopesando los dos esquemas, nuestra nota sobre SOCKS5 vs HTTP proxies cubre cuándo gana cada uno:
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 en colly
Si haces scraping con colly, obtienes rotación sin tocar el transporte. El paquete colly/proxy incluye un RoundRobinProxySwitcher que cicla endpoints por solicitud, y LimitRule maneja el ritmo. Esta es la forma más rápida de tener un crawler rotativo y educado en 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,
})
Concurrencia bien hecha (y la trampa de context-canceled)
La concurrencia de Go es la razón por la que los equipos lo eligen para scraping, y también de donde proviene el error context canceled. Se activa cuando el contexto de una solicitud se cancela antes de que el cuerpo se lea completamente - generalmente un tiempo de espera por solicitud que expiró, o un cancel() que se ejecutó demasiado pronto debido a un defer mal colocado. Limita tu fan-out con un semáforo, da a cada solicitud su propio contexto de tiempo de espera, y lee y cierra el cuerpo antes de que se active el cancel:
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()
Dos notas más de producción. Establece MaxIdleConnsPerHost lo suficientemente alto para que un gran pool reutilice conexiones en lugar de agotar los descriptores de archivo - la causa habitual de EOF después de unas pocas cientos de solicitudes. Y rota la salida en caso de fallo en lugar de intentar la misma de nuevo; una IP residencial muerta no se recupera en el siguiente intento. Para el diseño más amplio en millones de páginas, nuestra guía sobre arquitectura de scraping a gran escala cubre colas, deduplicación y jerarquización de proxies.
Reutiliza un cliente, ajusta el transporte
Crea el http.Client una vez y compártelo en cada goroutine - es seguro para uso concurrente, y su pool de conexiones es lo que hace que un scraper en Go sea rápido. Un cliente nuevo por solicitud descarta el keep-alive y fuerza un nuevo handshake TLS cada vez. Tres campos de Transport importan bajo carga: MaxIdleConns y MaxIdleConnsPerHost dimensionan el pool, y IdleConnTimeout retira conexiones obsoletas para que un pool de proxies rotativo nunca fije una salida muerta. Establécelos explícitamente - los valores predeterminados de la biblioteca estándar están ajustados para un navegador, no para un crawler que ejecuta miles de solicitudes por minuto.

Cuándo dejar de crear transportes a mano
El código anterior es suficiente para objetivos limpios. Una vez que un sitio añade Cloudflare, huellas digitales TLS o renderizado de JavaScript, un handshake TLS en Go no se parece en nada al de Chrome, y ningún proxy soluciona esa discrepancia. En ese punto, una Scraper API que lleva una huella digital de navegador real, rota IPs y renderiza JS por ti es menos código y una tasa de éxito más alta que mantener el stack a mano. Si estás eligiendo un lenguaje para un nuevo scraper, nuestra publicación sobre mejores proxies para web scraping mapea las compensaciones entre stacks.
Descarga el renderizado y la rotación a la Scraper API
Preguntas frecuentes
¿Cómo configuro un proxy para el cliente HTTP de Go?
Construye un http.Transport con un campo Proxy y pásalo a http.Client. Usa http.ProxyURL(u) para un proxy estático único, o una func(*http.Request) (*url.URL, error) para elegir la salida por solicitud. Pon credenciales en la URL como http://user:pass@host:port y siempre establece Client.Timeout.
¿Por qué Go ignora mi variable HTTP_PROXY?
Porque construiste un Transport personalizado con una función Proxy explícita, que reemplaza el comportamiento predeterminado de ProxyFromEnvironment. Las variables de entorno solo se aplican cuando usas http.DefaultTransport o estableces Proxy: http.ProxyFromEnvironment tú mismo. También ten en cuenta que las solicitudes a loopback y localhost siempre pasan por alto el proxy.
¿Qué causa 'context canceled' con un proxy en Go?
El contexto de la solicitud se canceló antes de que se leyera el cuerpo de la respuesta - típicamente un tiempo de espera por solicitud que expiró o un cancel() que se ejecutó demasiado pronto a través de defer. Da a cada solicitud su propio contexto de tiempo de espera, drena y cierra el cuerpo antes de que se ejecute el cancel diferido, y no compartas un contexto cancelable entre muchas goroutines.
¿Go soporta proxies SOCKS5?
No en la biblioteca estándar, pero golang.org/x/net/proxy añade un dialer SOCKS5. Créalo con proxy.SOCKS5 e intégralo en Transport.DialContext para que los nombres de host se resuelvan en el proxy en lugar de filtrarse a través de una búsqueda DNS local.
Esa es toda la imagen del lado del cliente: un campo Transport para la configuración, una función para la rotación, x/net/proxy para SOCKS5, colly para un rastreo educado, y un semáforo más contextos por solicitud para la concurrencia. Comienza con un pool rotativo limpio y evitas la mayoría de la lista de errores antes de que ocurra.