Tu servicio en Go tiene que enviar sus peticiones a través de un proxy: una salida en otro país, una IP fija que un socio ha puesto en su lista de IP autorizadas, o un pool para que una sola dirección no cargue con todas las peticiones. Go no necesita ningún paquete extra para esto; net/http trae el soporte de proxy integrado, SOCKS5 y las variables de entorno estándar incluidos. Las sorpresas están en los valores por defecto: http.Get ya lee HTTPS_PROXY, localhost nunca pasa por el proxy, las variables se leen una sola vez por proceso y una conexión HTTP/2 puede saltarse en silencio la función de proxy que escribiste para la rotación.
Esta guía cubre http.ProxyURL, ProxyFromEnvironment y las reglas de NO_PROXY, la cabecera Proxy-Authorization, SOCKS5 en las dos formas que ofrece Go, un proxy distinto por petición, timeouts, reintentos, los textos exactos de los errores y un descargador completo. Cada fragmento se ejecutó en Go 1.26.6 (golang.org/x/net v0.62.0) contra un proxy HTTP local que exige user:pass y un servidor SOCKS5 local; las salidas y las líneas de registro son reales, solo el host del proxy se sustituyó por pr.proxynet.io:8000. El lado Node.js del mismo trabajo está en Cómo usar un proxy en Node.js.
¿Cómo decide net/http qué proxy usar?
Cada petición pasa por un http.Transport, cuyo campo Proxy es una función que recibe la petición y devuelve la URL del proxy, o nil para una conexión directa. El esquema de la URL elige el tipo de proxy: la documentación de Transport en net/http enumera http, https, socks5 y socks5h, un esquema vacío significa http, y las credenciales de la URL se convierten en una cabecera Proxy-Authorization. http.DefaultTransport, el que está detrás de http.Get, pone http.ProxyFromEnvironment en Proxy; un Transport que construyes tú no usa proxy hasta que le pones uno.
Después, según el destino:
- El Transport se conecta al proxy, no al destino; si falta el puerto, usa 80 para
http, 443 parahttpsy 1080 para SOCKS. - Destino
http://sin cifrar. La petición va al proxy con la URL completa en la línea de petición, másProxy-Authorizationen cada petición. - Destino
https://. Go envíaCONNECT host:443con la cabecera una sola vez; tras el200del proxy, el handshake TLS se hace dentro del túnel y el proxy solo ve el host y el puerto. El método CONNECT está definido en la RFC 9110. - Proxy
socks5://. Go hace el handshake SOCKS, inicia sesión si la URL lleva credenciales y envía el nombre de host del destino, así que el DNS lo resuelve el proxy. - La conexión se guarda en el pool para la siguiente petición al mismo destino por el mismo proxy, con keep-alive de HTTP/1.1 o con HTTP/2, y una conexión reutilizada no vuelve a llamar a
Proxy.
¿Cómo se configura un proxy con http.ProxyURL?
http.ProxyURL envuelve una URL fija en una función con la forma adecuada. Parsea la dirección una vez, ponla en un Transport y dale el Transport a un Client que también lleve un timeout:
package main
import (
"cmp"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
// El Generador de endpoints del panel imprime esta cadena. Guárdala en una variable de entorno, no en git.
raw := cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000")
proxyURL, err := url.Parse(raw)
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 20 * time.Second,
}
for _, target := range []string{"https://httpbin.org/ip", "http://httpbin.org/ip"} {
resp, err := client.Get(target)
if err != nil {
fmt.Println("error:", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s -> %d %s", target, resp.StatusCode, body)
}
}https://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}
http://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}En el lado del proxy las dos difieren: un CONNECT para el destino HTTPS y un GET reenviado con la URL completa para el destino HTTP, ambos con credenciales:
13:25:51.052 [http-proxy] CONNECT httpbin.org:443 proxy-authorization=Basic user -> 200 (tunnel #1)
13:25:51.563 [http-proxy] GET http://httpbin.org/ip proxy-authorization=Basic user user-agent=Go-http-client/1.1 -> forwardDos hábitos: resp.Body.Close() después de cada respuesta, o la conexión no puede volver al pool, y un Timeout en el Client, o una salida que deja de responder retiene una goroutine para siempre. El User-Agent por defecto es Go-http-client/1.1; pon el tuyo para que el destino pueda identificar tu programa.
¿Cómo lee Go HTTP_PROXY, HTTPS_PROXY y NO_PROXY?
http.ProxyFromEnvironment lee las tres variables, o sus formas en minúsculas, en cada petición; las reglas están en la documentación del paquete httpproxy, la implementación que usa net/http:
HTTP_PROXYse aplica a destinoshttp://yHTTPS_PROXYa destinoshttps://: decide el esquema del destino, no el del proxy, así que una dirección de proxyhttp://enHTTPS_PROXYes normal.- Un valor es una URL completa o un
host[:port]a secas, que se trata comohttp; cualquier otra cosa hace fallar la petición. - Si existen la variable en mayúsculas y la variable en minúsculas, gana la de minúsculas.
NO_PROXYlista direcciones IP, rangos CIDR como10.0.0.0/8y nombres de dominio, cada uno con puerto opcional. Un dominio coincide consigo mismo y con sus subdominios; un nombre que empieza por punto coincide solo con los subdominios; un único*desactiva el proxy.localhosty las direcciones de loopback nunca pasan por el proxy.
Este programa pregunta a ProxyFromEnvironment por cinco destinos sin enviar nada:
package main
import (
"fmt"
"net/http"
)
func main() {
targets := []string{
"https://httpbin.org/ip",
"http://httpbin.org/ip",
"https://api.internal.example.com/v1",
"http://10.0.0.7:8080/health",
"http://localhost:9000/",
}
for _, target := range targets {
req, _ := http.NewRequest(http.MethodGet, target, nil)
proxyURL, err := http.ProxyFromEnvironment(req)
switch {
case err != nil:
fmt.Printf("%-36s error: %v\n", target, err)
case proxyURL == nil:
fmt.Printf("%-36s direct\n", target)
default:
fmt.Printf("%-36s via %s\n", target, proxyURL.Redacted())
}
}
}Con HTTPS_PROXY y HTTP_PROXY definidas y NO_PROXY=.example.com,10.0.0.0/8:
https://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health direct
http://localhost:9000/ directCon solo HTTP_PROXY, los destinos HTTPS van directos, la causa más común de que un programa en Go «ignore el proxy»:
https://httpbin.org/ip direct
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health via http://user:xxxxx@pr.proxynet.io:8000
http://localhost:9000/ directRedacted() enmascara la contraseña como xxxxx; úsalo en los registros.
Hay una regla que no está en la semántica de las variables: net/http lee el entorno una vez, en la primera petición, y lo guarda en caché durante toda la vida del proceso, así que un os.Setenv posterior no tiene efecto:
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000Para recoger un entorno cambiado en tiempo de ejecución, pon en Transport.Proxy la función httpproxy.FromEnvironment().ProxyFunc(), que lee las variables en cada llamada.
¿Cómo envía Go el usuario y la contraseña del proxy?
Cuando la URL del proxy lleva usuario y contraseña, Go envía Proxy-Authorization: Basic … por su cuenta. Con un destino http:// sin cifrar, la cabecera viaja con cada petición, incluso en una conexión keep-alive: en nuestra prueba de reintentos, cada uno de los cuatro GET de un mismo socket la llevaba. Con un destino https:// se envía una vez, en el CONNECT, y el túnel se reutiliza mientras viva la conexión.
Caracteres como @, : o / en la contraseña rompen la URL si no van codificados en porcentaje, así que construye la URL por partes:
proxyURL := &url.URL{
Scheme: "http",
Host: "pr.proxynet.io:8000",
User: url.UserPassword("user", "p@ss:word"), // en la red va codificada como p%40ss%3Aword
}Basic es el único esquema que Go envía a un proxy; NTLM o Kerberos necesita un paquete de terceros. El gateway de un proveedor acepta Basic o una lista de IP autorizadas, que no deja ninguna contraseña en la URL y encaja con un servidor de dirección fija; comparamos las dos en Autenticación de proxy: user:pass o lista blanca de IP.
¿Cómo se usa un proxy SOCKS5 en Go?
Hay dos formas. Transport.Proxy acepta una URL socks5:// y hace el handshake por sí mismo; Go trata socks5 exactamente igual que socks5h, así que el proxy resuelve el nombre de host del destino. El paquete golang.org/x/net/proxy, en cambio, te entrega el dialer SOCKS5 como un valor que conectas a Transport.DialContext y que también puedes usar en conexiones que no son HTTP.
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"time"
"golang.org/x/net/proxy"
)
func main() {
// SOCKS_URL=socks5://user:pass@pr.proxynet.io:<el puerto SOCKS5 que muestra el Generador de endpoints>
socksURL, err := url.Parse(os.Getenv("SOCKS_URL"))
if err != nil || socksURL.Host == "" {
log.Fatal("set SOCKS_URL to socks5://user:pass@host:port")
}
// 1. net/http gestiona socks5:// por su cuenta. El proxy resuelve el nombre de host.
viaTransport := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
Timeout: 20 * time.Second,
}
show("Transport.Proxy", viaTransport)
// 2. golang.org/x/net/proxy te da el propio dialer SOCKS5, válido para cualquier conexión TCP.
password, _ := socksURL.User.Password()
auth := &proxy.Auth{User: socksURL.User.Username(), Password: password}
dialer, err := proxy.SOCKS5("tcp", socksURL.Host, auth, &net.Dialer{Timeout: 10 * time.Second})
if err != nil {
log.Fatal(err)
}
viaDialer := &http.Client{
Transport: &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext},
Timeout: 20 * time.Second,
}
show("x/net/proxy dialer", viaDialer)
}
func show(label string, client *http.Client) {
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
fmt.Printf("%-19s error: %v\n", label, err)
return
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%-19s %d %s", label, resp.StatusCode, body)
}Transport.Proxy 200 {
"origin": "203.0.113.42"
}
x/net/proxy dialer 200 {
"origin": "203.0.113.42"
}Nuestro servidor SOCKS5 registró los mismos tres pasos con los dos clientes: Go ofreció los métodos «sin autenticación» y «usuario/contraseña», el servidor eligió usuario/contraseña tal como lo define la RFC 1929, y la petición de conexión llevaba el nombre de dominio, no una IP:
13:26:07.490 [socks5] greeting methods=[0,2]
13:26:07.490 [socks5] auth user=user -> OK
13:26:07.490 [socks5] CONNECT domain httpbin.org:443Con la vía del dialer, deja Transport.Proxy vacío; el dialer ya pasa por el proxy. SOCKS5 transporta TCP y UDP, pero net/http solo abre conexiones TCP. Los Proxies SOCKS5 de Proxynet funcionan en su propio puerto junto al puerto HTTP, con el mismo usuario y la misma contraseña.
¿Qué método encaja en cada situación?
| Método | Dónde va | Alcance | Nota |
|---|---|---|---|
http.ProxyURL(u) | Transport.Proxy | ese Transport | un proxy fijo |
http.ProxyFromEnvironment | Transport.Proxy (por defecto en DefaultTransport) | el proceso | se lee una vez; localhost queda fuera |
tu propia func(*http.Request) | Transport.Proxy | por petición | rotación, reglas por host |
URL socks5:// | cualquiera de las tres anteriores | ese Transport | el DNS se resuelve en el proxy |
dialer de x/net/proxy | Transport.DialContext | ese Transport | también TCP puro; deja Proxy vacío |
¿Cómo se usa un proxy distinto en cada petición?
Transport.Proxy es una función normal, así que puede devolver una URL distinta en cada llamada. Esta elige por turnos de una lista e imprime su elección:
var next atomic.Uint64
transport := &http.Transport{
// Turno rotatorio sobre la lista. Se llama cuando el Transport necesita una conexión para una petición.
Proxy: func(req *http.Request) (*url.URL, error) {
u := proxies[(next.Add(1)-1)%uint64(len(proxies))]
fmt.Printf("Proxy func: %s %s via %s\n", req.Method, req.URL, u.Redacted())
return u, nil
},
DisableKeepAlives: true, // una conexión nueva, y por tanto una elección de proxy nueva, en cada petición
}La última línea es la que importa. Enviamos cuatro GET a https://httpbin.org/ip, que negocia HTTP/2, con dos proxies en la lista, y contamos las llamadas y los túneles:
| Ajustes del Transport | Llamadas a la función Proxy | Túneles abiertos |
|---|---|---|
| Valores por defecto (HTTP/2 negociado) | 1 | 1, solo el primer proxy |
Solo HTTP/1.1 (mapa TLSNextProto vacío) | 4 | 2, uno por proxy, reutilizados |
DisableKeepAlives: true | 4 | 4, alternando |
Con los valores por defecto, Go abrió una conexión HTTP/2 por el primer proxy y multiplexó las cuatro peticiones por ella; la función no volvió a consultarse. Con keep-alive de HTTP/1.1 se consultó cada vez, pero las peticiones tres y cuatro reutilizaron la conexión inactiva con su proxy. Solo DisableKeepAlives, o req.Close = true, da un túnel por petición. Un gateway rotativo sigue la misma regla, porque la IP de salida se elige al abrir el túnel: un gateway de Proxies residenciales cambia la salida en cada túnel nuevo a través de una sola dirección, y un parámetro de sesión en el nombre de usuario mantiene una salida entre 1 y 60 minutos para los flujos con inicio de sesión.
Timeouts, límites de conexiones y TLS
http.Client.Timeout es el único ajuste que nunca debe quedarse en cero: cubre la petición entera, desde la conexión hasta el último byte. Los ajustes finos viven en el Transport: DialContext con un net.Dialer{Timeout: …} acota la conexión al proxy, TLSHandshakeTimeout el handshake dentro del túnel, ResponseHeaderTimeout la espera de las cabeceras; http.NewRequestWithContext con un plazo hace lo mismo para una sola petición.
Tres campos deciden cuántas conexiones abres. MaxConnsPerHost limita las conexiones por host y hace esperar a las demás, un limitador de velocidad barato por destino. MaxIdleConnsPerHost vale 2 por defecto; con más workers, las conexiones terminadas se cierran en lugar de guardarse en el pool, así que ponlo igual al número de workers. IdleConnTimeout cierra las conexiones que llevan tiempo sin usarse.
En TLS, no recurras nunca a InsecureSkipVerify. x509: certificate signed by unknown authority detrás de un proxy significa que un proxy corporativo inspecciona el TLS con su propia autoridad de certificación; añade esa CA a tls.Config.RootCAs. El gateway de un proveedor nunca toca el TLS de dentro del túnel, así que ahí el error apunta al certificado del destino.
Un ejemplo completo: reintentos, backoff y límite de concurrencia
El descargador de abajo combina las piezas: los timeouts y los límites de pool anteriores, reintentos ante errores de red, 429 y 5xx con backoff exponencial y jitter, respeto a Retry-After, ningún reintento ante un 407 y como máximo cuatro peticiones a la vez:
// fetch.go - hace GET a una lista de URL a través de un proxy con timeouts, reintentos, backoff y límite de concurrencia.
package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"log"
"math/rand/v2"
"net"
"net/http"
"net/url"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
const (
maxAttempts = 4 // primer intento + 3 reintentos
maxInFlight = 4 // peticiones en ejecución al mismo tiempo
)
var (
retryStatus = map[int]bool{429: true, 500: true, 502: true, 503: true, 504: true}
errProxyAuth = errors.New("proxy rejected the credentials (407): check user:pass or the IP whitelist")
)
func newClient(proxyURL *url.URL) *http.Client {
return &http.Client{
Timeout: 30 * time.Second, // la petición entera: conexión, cabeceras y cuerpo
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
MaxConnsPerHost: maxInFlight,
MaxIdleConnsPerHost: maxInFlight,
IdleConnTimeout: 60 * time.Second,
// Un 407 en el túnel CONNECT se convierte en un error tipado en lugar de "Proxy Authentication Required".
OnProxyConnectResponse: func(_ context.Context, _ *url.URL, _ *http.Request, res *http.Response) error {
if res.StatusCode == http.StatusProxyAuthRequired {
return errProxyAuth
}
return nil
},
},
}
}
// backoff: 1, 2, 4 s ... con tope en 30 s, más hasta 1 s de jitter para que los workers no reintenten al unísono.
func backoff(attempt int) time.Duration {
d := min(time.Duration(1<<(attempt-1))*time.Second, 30*time.Second)
return d + time.Duration(rand.IntN(1000))*time.Millisecond
}
func sleep(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
case <-time.After(d):
}
}
// fetch hace GET a una URL. Los errores de red, 429 y 5xx se reintentan; un 407 no, porque las
// credenciales son incorrectas y otro intento no puede arreglarlo.
func fetch(ctx context.Context, client *http.Client, target string) ([]byte, int, error) {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("User-Agent", "price-monitor/1.0 (+mailto:you@example.com)")
resp, err := client.Do(req)
if err != nil {
if errors.Is(err, errProxyAuth) || ctx.Err() != nil {
return nil, 0, err
}
lastErr = err
log.Printf("attempt %d/%d %s: %v", attempt, maxAttempts, target, err)
sleep(ctx, backoff(attempt))
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusProxyAuthRequired { // destino http sin cifrar: el 407 es una respuesta
return body, resp.StatusCode, errProxyAuth
}
if !retryStatus[resp.StatusCode] {
return body, resp.StatusCode, err // 200, 404, 301 ... decide quien llama
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
if attempt == maxAttempts {
break
}
// El sitio nos pidió ir más despacio. Su propio Retry-After manda sobre nuestro calendario.
wait := backoff(attempt)
if secs, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(secs) * time.Second
}
log.Printf("attempt %d/%d %s: HTTP %d, sleeping %s", attempt, maxAttempts, target, resp.StatusCode, wait.Round(100*time.Millisecond))
sleep(ctx, wait)
}
return nil, 0, lastErr
}
func main() {
proxyURL, err := url.Parse(cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000"))
if err != nil {
log.Fatal(err)
}
targets := os.Args[1:]
if len(targets) == 0 {
for i := 1; i <= 4; i++ {
targets = append(targets, fmt.Sprintf("https://httpbin.org/ip?n=%d", i))
}
targets = append(targets, "https://httpbin.org/status/503")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := newClient(proxyURL)
sem := make(chan struct{}, maxInFlight)
var wg sync.WaitGroup
var ok, failed atomic.Int32
for _, target := range targets {
wg.Add(1)
sem <- struct{}{} // se bloquea mientras haya maxInFlight peticiones en curso
go func() {
defer wg.Done()
defer func() { <-sem }()
started := time.Now()
body, status, err := fetch(ctx, client, target)
if err != nil {
failed.Add(1)
log.Printf("fail %s -> %v", target, err)
return
}
ok.Add(1)
log.Printf("ok %s -> %d, %d bytes in %s", target, status, len(body), time.Since(started).Round(100*time.Millisecond))
}()
}
wg.Wait()
log.Printf("done: %d ok, %d failed", ok.Load(), failed.Load())
}Cinco URL, cuatro workers, una dirección que siempre responde 503 para ejercitar la ruta de reintentos:
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=1 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=2 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=4 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 attempt 1/4 https://httpbin.org/status/503: HTTP 503, sleeping 1.3s
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=3 -> 200, 32 bytes in 700ms
2026/10/28 16:30:58 attempt 2/4 https://httpbin.org/status/503: HTTP 503, sleeping 2.8s
2026/10/28 16:31:00 attempt 3/4 https://httpbin.org/status/503: HTTP 503, sleeping 4.8s
2026/10/28 16:31:05 fail https://httpbin.org/status/503 -> HTTP 503
2026/10/28 16:31:05 done: 4 ok, 1 failedTres decisiones de diseño. OnProxyConnectResponse se ejecuta cuando el proxy responde al CONNECT, antes de que Go convierta un estado distinto de 200 en una simple cadena de error; un error tipado ahí permite que errors.Is detenga el bucle de reintentos, y con una contraseña incorrecta el programa imprimió fail … -> proxy rejected the credentials (407) para un destino HTTPS y otro HTTP sin cifrar sin un solo reintento. El conjunto de reintentos es pequeño a propósito: un 404 o un 403 se devuelve tal cual, porque otra salida no cambia lo que es la página. Contra un servidor local que enviaba Retry-After: 2, el bucle durmió exactamente 2 s entre intentos en lugar de seguir su propio calendario.
¿Cómo se comprueba que la petición usa de verdad el proxy?
Pregunta a un servicio de eco qué dirección ve, una vez con Proxy: nil y otra con el proxy. El nil es deliberado: un Transport sin valor en Proxy nunca lee el entorno, así que la primera línea es tu propia dirección incluso con HTTPS_PROXY definida.
direct := &http.Client{Transport: &http.Transport{Proxy: nil}, Timeout: 20 * time.Second}
proxied := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, Timeout: 20 * time.Second}
fmt.Println("without proxy:", exitIP(direct)) // exitIP hace GET a https://api.ipify.org?format=json y devuelve el campo "ip"
fmt.Println("with proxy: ", exitIP(proxied))Con un gateway real, la segunda línea muestra la dirección de la salida. Con nuestro proxy de prueba, las dos líneas imprimieron la misma dirección, porque el proxy corre en la misma máquina; la prueba fue la línea CONNECT api.ipify.org:443 de su registro. Dos líneas idénticas con un proxy real suelen significar HTTP_PROXY sin HTTPS_PROXY, un destino incluido en NO_PROXY o un Transport creado sin valor en Proxy.
¿Y qué pasa con Colly?
Colly, el framework de crawling más usado en Go, se construye sobre http.Transport, así que todo lo anterior se aplica. Su paquete proxy incluye un RoundRobinProxySwitcher que recibe URL http://, https:// o socks5:// y devuelve una colly.ProxyFunc, la misma firma que Transport.Proxy; lo conectas con c.SetProxyFunc(rp), como en el ejemplo del proxy switcher de la documentación de Colly. El switcher se llama en cada petición, pero la regla de reutilización de conexiones de la sección de rotación sigue decidiendo si una petición abre un túnel nuevo.
Errores comunes y qué significan
Los mensajes tal como los imprimió Go 1.26.6 en nuestras pruebas, con el host de ejemplo:
Get "https://httpbin.org/ip": Proxy Authentication Requiredcon un destino HTTPS, o estado407como respuesta normal con un destino HTTP sin cifrar:user:passincorrecto, caracteres especiales sin codificar o una IP que no está en la lista de IP autorizadas. Las causas están reunidas en Cómo solucionar HTTP 407 Proxy Authentication Required.proxyconnect tcp: dial tcp pr.proxynet.io:8000: …más el texto de conexión rechazada del sistema operativo (connect: connection refuseden Linux y macOS,connectex:y una frase localizada en Windows): ahí no escucha nada.proxyconnectindica que el fallo ocurrió de camino al proxy, no al destino.socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: una URLsocks5://apuntaba al puerto HTTP; 72 es el valor en bytes de laHdeHTTP/1.1.socks connect tcp …: username/password authentication failed: el servidor SOCKS5 rechazó las credenciales.unexpected EOF: una URLhttp://en el puerto SOCKS5, que cerró la conexión al recibir elCONNECT; un esquema desconocido comosocks4://se trata comohttpy termina igual.proxyconnect tcp: tls: first record does not look like a TLS handshake: un esquema de proxyhttps://en un puerto HTTP normal; usahttp://salvo que el proveedor diga que el puerto habla TLS.- El proxy simplemente no se usa:
HTTP_PROXYsinHTTPS_PROXY, un destino que eslocalhosto está enNO_PROXY, variables definidas después de la primera petición o un Transport construido sin valor enProxy.
Dónde se usa esta configuración
- Comprobaciones de precio y disponibilidad desde varios países: un parámetro de país en el nombre de usuario del proxy y rotación por petición para páginas de producto independientes.
- Clientes de API detrás de una dirección autorizada, cuando un socio ha puesto una IP estática en su lista y el servicio tiene que salir por ella.
- Seguimiento desde fuera de tu propia red, para que una comprobación de salud vea el sitio como lo ve un visitante de otra región.
- Workers en una red corporativa que solo llegan a internet a través del proxy de la empresa.
Errores habituales
- Definir solo
HTTP_PROXY. Los destinos HTTPS van directos; define tambiénHTTPS_PROXY. - Cambiar el entorno después de la primera petición. net/http lo guarda en caché; reinicia el proceso o usa
httpproxy. - Esperar rotación con los keep-alive activados. Una conexión reutilizada nunca vuelve a llamar a
Proxy. - Olvidar
resp.Body.Close(). Las conexiones nunca vuelven al pool. - No poner
Client.Timeout. Una salida silenciosa bloquea una goroutine para siempre. - Pegar una contraseña con
@o:en la cadena de la URL. Constrúyela conurl.UserPassword. - Definir a la vez
Transport.Proxyy unDialContextSOCKS5. Elige uno. - Reintentar un
407. El segundo intento envía las mismas credenciales incorrectas.
¿Qué enfoque conviene elegir?
| Tu situación | Recomendación |
|---|---|
| Un proxy, sin cambiar el código | HTTPS_PROXY y HTTP_PROXY, NO_PROXY para los hosts internos |
| Un proxy, definido en el código | Transport{Proxy: http.ProxyURL(u)} más Client.Timeout |
| Una salida nueva en cada petición | Tu propia función Proxy o un gateway rotativo, DisableKeepAlives: true |
| La misma salida para un flujo con inicio de sesión | Un parámetro de sesión en el nombre de usuario, keep-alive activado |
| SOCKS5 con DNS en el proxy | socks5://user:pass@host:port en Transport.Proxy |
| SOCKS5 para una conexión que no es HTTP | Dialer de golang.org/x/net/proxy |
| Muchos workers sobre un mismo sitio | MaxConnsPerHost y MaxIdleConnsPerHost = número de workers |
| Proxy corporativo con CA propia | La CA en tls.Config.RootCAs, nunca InsecureSkipVerify |
Preguntas frecuentes
¿http.Get en Go usa un proxy automáticamente?
Sí, cuando el entorno lo indica: http.Get usa http.DefaultClient, cuyo Transport pone http.ProxyFromEnvironment en Proxy. Un Transport que creas tú empieza sin proxy.
¿Es lo mismo que GOPROXY?
No. GOPROXY le dice al comando go de dónde descargar los módulos; no tiene nada que ver con las peticiones que envía tu programa en tiempo de ejecución.
¿Cómo pongo un proxy solo para una petición?
Dale a esa petición su propio Client y Transport, o escribe una función Proxy que devuelva nil para las peticiones que deben ir directas. Puede que ya exista una conexión en el pool hacia el mismo host; req.Close = true fuerza una nueva.
¿Go admite socks5h?
Sí, y socks5 se comporta igual: Go siempre envía el nombre de host al proxy, que lo resuelve; net/http no tiene un modo SOCKS5 con resolución local.
¿Por qué localhost nunca pasa por el proxy?
ProxyFromEnvironment no devuelve proxy para localhost ni para las direcciones de loopback, digan lo que digan las variables; para pasar esas peticiones por el proxy, usa http.ProxyURL.
¿Cómo veo qué proxy usó una petición?
Envuelve la función Proxy y registra u.Redacted(), o define Transport.OnProxyConnectResponse, que recibe la URL del proxy y la respuesta al CONNECT. Con los keep-alive activados, la elección se registra solo cuando se abre una conexión nueva.
En resumen
En Go, el proxy es una función de http.Transport: http.ProxyURL para una dirección fija, http.ProxyFromEnvironment para las variables estándar o tu propia función para elegir por petición. Pon user:pass en la URL y Go envía Proxy-Authorization por su cuenta; usa socks5:// para SOCKS5; define Client.Timeout y los límites del pool; reintenta 429 y 5xx con backoff, pero nunca un 407; desactiva los keep-alive cuando cada petición necesite una salida nueva. Los endpoints residenciales, ISP estáticos, de centro de datos y SOCKS5 que funcionan con estos ajustes están en la página de proxies de Proxynet.




