# 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:

```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
}
```

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:

```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
}
```

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](https://quantumproxies.io/rotating-proxies) 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.

![Un único http.Client de Go con un único Transport cuya función Proxy se ejecuta en cada solicitud, de modo que las solicitudes 1, 2 y 3 salen por IPs de salida distintas hacia el mismo objetivo. La tarjeta: la función se llama una vez por solicitud, elige de un pool o apunta a un gateway rotativo, y MaxIdleConnsPerHost: 32 mantiene la reutilización de las conexiones.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335148/blog/generated/qp-b9-009-diag1.png)
*Un Transport, una función Proxy: la función se ejecuta por solicitud, por lo que un solo cliente rota a través de todo el pool.*

## 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](https://quantumproxies.io/blog/socks5-vs-http-proxy-difference) cubre cuándo gana cada uno:

```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}
```

## 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:

```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,
})
```

## 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:

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

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](https://quantumproxies.io/blog/large-scale-web-scraping-architecture) 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.

![Una tabla de errores de proxy en Go con la columna de causas resaltada. La tarjeta asigna cada error a su causa: proxyconnect o 407 significa credenciales incorrectas, context canceled significa que cancel() se ejecutó antes de leer el cuerpo, EOF significa que MaxIdleConnsPerHost es demasiado bajo, un HTTP_PROXY ignorado significa un Transport personalizado; un bloqueo indefinido, falta de timeout.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335148/blog/generated/qp-b9-009-diag2.png)
*Casi todos los fallos de proxy en Go se mapean a uno de estos cinco - revisa la columna de causa antes de intentar la misma salida de nuevo.*

## 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](https://quantumproxies.io/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](https://quantumproxies.io/blog/best-proxies-for-web-scraping) mapea las compensaciones entre stacks.

[Descarga el renderizado y la rotación a la Scraper API](https://quantumproxies.io/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.

[Comienza con proxies residenciales de QuantumProxies](https://quantumproxies.io/residential-proxies)
