Si estás escribiendo un scraper, automatizando la recolección de datos o probando APIs desde diferentes regiones, tarde o temprano te encontrarás con bloqueos por IP. Go es uno de los lenguajes más populares para escribir herramientas de red de alto rendimiento, y la configuración de proxies aquí tiene sus particularidades. En esta guía, cubriremos todo, desde la configuración básica de http.Client hasta la rotación avanzada de IP con soporte para SOCKS5.
¿Por qué se necesitan proxies en aplicaciones Go?
Go se ha convertido en el estándar de facto para escribir herramientas de red de alto rendimiento: scrapers, crawlers, bots para monitorear precios, herramientas de automatización de APIs. Es aquí donde la necesidad de proxies surge con mayor frecuencia, y es aquí donde es importante configurar todo correctamente desde el principio.
Los principales escenarios en los que no se puede prescindir de un proxy son:
- Scraping de marketplaces — Wildberries, Ozon, AliExpress bloquean IP después de 50–200 solicitudes desde una sola dirección. Sin cambio de IP, el scraper se detiene.
- Geo-testing — verificación de resultados de motores de búsqueda, anuncios publicitarios o precios desde diferentes países y regiones.
- Eludir rate-limit — distribuir la carga entre varias IP permite eludir las restricciones en el número de solicitudes por minuto.
- Anonimato — ocultar la IP real del servidor al trabajar con APIs externas.
- Pruebas de CDN y geolocalización — verificar la correcta entrega de contenido para diferentes regiones.
La biblioteca estándar de Go proporciona herramientas flexibles para trabajar con proxies — a través de http.Transport y http.Client. Vamos a analizar cada método en detalle.
Configuración básica de un proxy HTTP a través de http.Client
En Go, un proxy se configura a nivel de http.Transport — la capa de transporte del cliente HTTP. Esto proporciona un control total sobre qué proxy se utilizan para las solicitudes y permite crear varios clientes con diferentes proxies simultáneamente.
Un ejemplo mínimo de configuración de un proxy HTTP:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
// Especificamos la dirección del servidor proxy
proxyURL, err := url.Parse("http://123.45.67.89:8080")
if err != nil {
panic(err)
}
// Creamos un transporte con el proxy
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
// Creamos un cliente con nuestro transporte
client := &http.Client{
Transport: transport,
}
// Hacemos una solicitud a través del proxy
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Lo que sucede en este código:
url.Parse— analiza la cadena con la dirección del proxy en una estructura*url.URLhttp.Transport{Proxy: http.ProxyURL(proxyURL)}— crea un transporte que dirige todas las solicitudes a través del proxy especificadohttp.Client{Transport: transport}— crea un cliente con nuestro transporte
Un punto importante: si utilizas http.DefaultClient (es decir, simplemente http.Get() sin un cliente explícito), el proxy no se aplicará. Siempre crea tu propio http.Client para trabajar con proxies.
💡 Consejo
Agrega un timeout en http.Client: Timeout: 30 * time.Second. Los servidores proxy pueden responder más lentamente que una conexión directa, y sin un timeout, las goroutines se quedarán colgadas indefinidamente.
Proxy a través de variables de entorno
Go soporta variables de entorno estándar para proxies — HTTP_PROXY, HTTPS_PROXY y NO_PROXY. El transporte estándar http.DefaultTransport los lee automáticamente a través de la función http.ProxyFromEnvironment.
Configuración de variables de entorno antes de ejecutar la aplicación:
# En la terminal (Linux/macOS)
export HTTP_PROXY="http://123.45.67.89:8080"
export HTTPS_PROXY="http://123.45.67.89:8080"
export NO_PROXY="localhost,127.0.0.1"
# Ejecutamos la aplicación
go run main.go
Si deseas usar ProxyFromEnvironment en tu propio transporte (por ejemplo, con configuraciones adicionales de TLS), hazlo así:
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
// Configuraciones adicionales...
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
}
client := &http.Client{
Transport: transport,
Timeout: 60 * time.Second,
}
Este enfoque es conveniente para entornos de producción, donde la dirección del proxy se establece a través de la configuración de despliegue (Docker, Kubernetes, systemd), y no se codifica directamente en el código. También facilita el cambio de proxies sin recompilar la aplicación.
Conexión de un proxy SOCKS5 en Golang
SOCKS5 es un protocolo más versátil en comparación con los proxies HTTP. Funciona a un nivel más bajo y soporta cualquier conexión TCP/UDP, no solo HTTP/HTTPS. Para trabajar con scrapers y automatización, SOCKS5 a menudo es preferible, especialmente al usar proxies residenciales, que a menudo se proporcionan precisamente a través de este protocolo.
Para trabajar con SOCKS5 en Go, se necesita el paquete golang.org/x/net/proxy:
go get golang.org/x/net/proxy
Ejemplo de conexión a un proxy SOCKS5 sin autenticación:
package main
import (
"fmt"
"io"
"net/http"
"golang.org/x/net/proxy"
)
func main() {
// Creamos un dialer SOCKS5
dialer, err := proxy.SOCKS5("tcp", "123.45.67.89:1080", nil, proxy.Direct)
if err != nil {
panic(err)
}
// Creamos un transporte con el dialer personalizado
transport := &http.Transport{
Dial: dialer.Dial,
}
client := &http.Client{
Transport: transport,
}
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
A partir de Go 1.20+, también se puede utilizar el soporte integrado para SOCKS5 a través de url.Parse con el prefijo socks5://:
proxyURL, _ := url.Parse("socks5://123.45.67.89:1080")
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
client := &http.Client{Transport: transport}
Esta sintaxis es más sencilla y no requiere dependencias adicionales. Sin embargo, para escenarios más complejos (por ejemplo, SOCKS5 con autenticación en versiones anteriores de Go), el paquete golang.org/x/net/proxy sigue siendo relevante.
Autenticación: nombre de usuario y contraseña en el proxy
La mayoría de los proveedores de proxies comerciales utilizan autenticación mediante nombre de usuario y contraseña. Hay dos formas de pasar las credenciales en Go.
Forma 1: Incrustar en la URL
// Formato: protocol://username:password@host:port
proxyURL, err := url.Parse("http://myuser:[email protected]:8080")
if err != nil {
panic(err)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
Forma 2: A través de la estructura url.URL (más seguro)
proxyURL := &url.URL{
Scheme: "http",
Host: "proxy.example.com:8080",
User: url.UserPassword("myuser", "mypassword"),
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
La segunda forma es preferible, ya que maneja correctamente los caracteres especiales en la contraseña (por ejemplo, @, #, :), que pueden romper el análisis de la URL en la primera opción.
Para SOCKS5 con autenticación a través del paquete golang.org/x/net/proxy:
auth := &proxy.Auth{
User: "myuser",
Password: "mypassword",
}
dialer, err := proxy.SOCKS5("tcp", "proxy.example.com:1080", auth, proxy.Direct)
if err != nil {
panic(err)
}
transport := &http.Transport{
Dial: dialer.Dial,
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
⚠️ Seguridad
Nunca codifiques el nombre de usuario y la contraseña del proxy directamente en el código. Utiliza variables de entorno o archivos de configuración que no se incluyan en el repositorio. Agrega .env a .gitignore.
Rotación de proxies: cambio de IP entre solicitudes
Un proxy es bueno para pruebas, pero para scraping real se necesita rotación de IP. Los sitios rastrean la frecuencia de solicitudes desde una sola dirección, y incluso un buen proxy será bloqueado si se envían miles de solicitudes consecutivas a través de él.
Hay dos enfoques para la rotación en Go:
Enfoque 1: Lista de proxies con round-robin
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"sync/atomic"
"time"
)
type ProxyRotator struct {
proxies []*url.URL
counter uint64
}
func NewProxyRotator(proxyURLs []string) (*ProxyRotator, error) {
proxies := make([]*url.URL, 0, len(proxyURLs))
for _, p := range proxyURLs {
u, err := url.Parse(p)
if err != nil {
return nil, err
}
proxies = append(proxies, u)
}
return &ProxyRotator{proxies: proxies}, nil
}
// GetClient devuelve un cliente con el siguiente proxy en el ciclo
func (r *ProxyRotator) GetClient() *http.Client {
idx := atomic.AddUint64(&r.counter, 1) % uint64(len(r.proxies))
proxyURL := r.proxies[idx]
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
return &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
}
func main() {
proxyList := []string{
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
}
rotator, err := NewProxyRotator(proxyList)
if err != nil {
panic(err)
}
// Cada solicitud pasa a través del siguiente proxy
for i := 0; i < 10; i++ {
client := rotator.GetClient()
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
fmt.Printf("Error en la solicitud %d: %v\n", i, err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("Solicitud %d: %s\n", i, string(body))
}
}
Enfoque 2: Selección dinámica de proxies a través de una función
El campo Proxy en http.Transport acepta una función con la firma func(*http.Request) (*url.URL, error). Esto permite seleccionar proxies dinámicamente para cada solicitud:
proxies := []*url.URL{
mustParseURL("http://proxy1.example.com:8080"),
mustParseURL("http://proxy2.example.com:8080"),
mustParseURL("http://proxy3.example.com:8080"),
}
var counter uint64
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
// Elegimos un proxy basado en la solicitud (round-robin)
idx := atomic.AddUint64(&counter, 1) % uint64(len(proxies))
return proxies[idx], nil
},
}
client := &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
func mustParseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}
El segundo enfoque es más elegante: un cliente rota automáticamente los proxies para cada solicitud. Esto es especialmente conveniente para solicitudes concurrentes a través de goroutines.
Qué tipo de proxy elegir para un proyecto Go
La elección del tipo de proxy influye directamente en el éxito de tu proyecto. Diferentes tareas requieren diferentes tipos — analicemos las opciones principales:
| Tipo de proxy | Velocidad | Anonimato | Mejor para |
|---|---|---|---|
| Centro de datos | ⚡ Alta | Media | Scraping de datos abiertos, pruebas de API, pruebas de carga |
| Residenciales | 🔄 Media | Alta | Scraping de marketplaces (Wildberries, Ozon), eludir protección anti-bots |
| Móviles | 🔄 Media | Máxima | Trabajo con redes sociales, servicios con estricta protección anti-bots |
Para scraping de marketplaces (Wildberries, Ozon, Avito): utiliza proxies residenciales. Estas plataformas utilizan activamente sistemas de detección de bots (Cloudflare, soluciones propias) y bloquean fácilmente las IP de centros de datos. Las IP residenciales parecen usuarios domésticos normales.
Para pruebas de API y pruebas de carga: los proxies de centros de datos son la opción óptima. Alta velocidad, conexión estable, latencia predecible.
Para geo-testing: proxies residenciales con selección de país/ciudad. Permiten verificar cómo se ve tu servicio o anuncio para usuarios de una región específica.
Errores comunes y mejores prácticas
A lo largo de los años trabajando con proxies en proyectos de Go, se ha cristalizado una lista de errores típicos que ralentizan el desarrollo y disminuyen la fiabilidad de los scrapers. Analicemos cada uno.
Error 1: Reutilización del transporte sin restablecer conexiones
http.Transport almacena en caché las conexiones TCP (keep-alive). Si creas un nuevo transporte para cada solicitud, pierdes esta ventaja. Pero si reutilizas un transporte fijo con un proxy al rotar, las solicitudes pueden ir a través de una conexión antigua, omitiendo el nuevo proxy.
Solución: al cambiar de proxy, llama a transport.CloseIdleConnections() o utiliza un enfoque funcional con selección dinámica de proxies (descrito en la sección sobre rotación).
Error 2: No hay manejo de errores de proxy
Los proxies pueden estar inactivos, sobrecargados o bloqueados por el sitio objetivo. Sin lógica de reintento, tu scraper se detendrá en el primer error.
func fetchWithRetry(client *http.Client, targetURL string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
resp, err := client.Get(targetURL)
if err != nil {
lastErr = err
time.Sleep(time.Duration(i+1) * time.Second) // Retraso exponencial
continue
}
defer resp.Body.Close()
if resp.StatusCode == 429 || resp.StatusCode == 403 {
lastErr = fmt.Errorf("bloqueado: estado %d", resp.StatusCode)
time.Sleep(time.Duration(i+1) * 2 * time.Second)
continue
}
return io.ReadAll(resp.Body)
}
return nil, fmt.Errorf("todas las intentos agotados: %w", lastErr)
}
Error 3: Ignorar la verificación de TLS
Algunos desarrolladores desactivan la verificación de certificados TLS para proxies (InsecureSkipVerify: true). Esto crea una vulnerabilidad para ataques MITM. Si el servidor proxy utiliza un certificado autofirmado, agrégalo al conjunto de confianza, en lugar de desactivar la verificación por completo.
Error 4: Mismos encabezados para todas las solicitudes
Incluso con la rotación de proxies, puedes ser bloqueado por el patrón de encabezados. Cambia el User-Agent junto con el proxy, agrega encabezados realistas de navegador:
userAgents := []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/118.0.0.0 Safari/537.36",
}
req, _ := http.NewRequest("GET", targetURL, nil)
req.Header.Set("User-Agent", userAgents[rand.Intn(len(userAgents))])
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
Lista de verificación para un cliente proxy confiable en Go
- ✅ Timeout en el cliente (
Timeout: 30 * time.Second) - ✅ Lógica de reintento con retraso exponencial
- ✅ Rotación de proxies en errores 403/429
- ✅ Rotación de User-Agent junto con el proxy
- ✅ Registro de errores con indicación del proxy utilizado
- ✅ Credenciales a través de variables de entorno, no codificadas
- ✅ Cierre de
resp.Bodya través dedefer - ✅ Limitación de concurrencia a través de un semáforo o
worker pool
Ejemplo de un worker pool completo con rotación de proxies
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"sync"
"sync/atomic"
"time"
)
type Scraper struct {
proxies []*url.URL
counter uint64
workers int
}
func (s *Scraper) getClient() *http.Client {
idx := atomic.AddUint64(&s.counter, 1) % uint64(len(s.proxies))
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(s.proxies[idx]),
},
Timeout: 30 * time.Second,
}
}
func (s *Scraper) Scrape(urls []string) map[string]string {
results := make(map[string]string)
var mu sync.Mutex
var wg sync.WaitGroup
sem := make(chan struct{}, s.workers) // Semáforo para limitar la concurrencia
for _, u := range urls {
wg.Add(1)
go func(targetURL string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
client := s.getClient()
resp, err := client.Get(targetURL)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
mu.Lock()
results[targetURL] = string(body)
mu.Unlock()
}(u)
}
wg.Wait()
return results
}
Conclusión
Configurar proxies en Go es una tarea que se resuelve con unas pocas líneas de código, pero requiere comprensión de los matices: el uso correcto de http.Transport, el manejo adecuado de la autenticación, la rotación inteligente de IP y el manejo de errores. La biblioteca estándar proporciona todo lo necesario para proxies HTTP, y el paquete golang.org/x/net/proxy cubre la necesidad de SOCKS5.
Puntos clave de la artículo:
- Siempre crea tu propio
http.Client—http.DefaultClientno soporta proxies directamente - Utiliza un enfoque funcional (
Proxy: func(r *Request) (*url.URL, error)) para la rotación dinámica - Almacena las credenciales en variables de entorno, no en el código
- Agrega lógica de reintento — los proxies no son 100% confiables
- Cambia el User-Agent junto con el proxy para máxima efectividad
Si estás escribiendo un scraper para marketplaces como Wildberries o Ozon, te recomendamos utilizar proxies residenciales — tienen IP reales de usuarios domésticos y son mucho menos propensos a ser bloqueados por sistemas anti-bots. Para escenarios de alta carga con miles de solicitudes por minuto, la opción óptima serán proxies de centros de datos — proporcionan la máxima velocidad y estabilidad de conexión.
```