Seu serviço em Go precisa enviar as requisições por um proxy: uma saída em outro país, um IP fixo que um parceiro colocou na whitelist, ou um pool para que um único endereço não carregue todas as requisições. O Go não precisa de nenhum pacote extra para isso; o net/http traz o suporte a proxy embutido, incluindo SOCKS5 e as variáveis de ambiente padrão. As surpresas estão nos padrões: o http.Get já lê HTTPS_PROXY, localhost nunca passa pelo proxy, as variáveis são lidas uma só vez por processo e uma conexão HTTP/2 pode pular em silêncio a função de proxy que você escreveu para a rotação.
Este guia cobre http.ProxyURL, ProxyFromEnvironment e as regras de NO_PROXY, o cabeçalho Proxy-Authorization, SOCKS5 nas duas formas que o Go oferece, um proxy diferente por requisição, timeouts, novas tentativas, os textos exatos dos erros e um downloader completo. Cada trecho rodou no Go 1.26.6 (golang.org/x/net v0.62.0) contra um proxy HTTP local que exige user:pass e um servidor SOCKS5 local; as saídas e as linhas de log são reais, só o host do proxy foi substituído por pr.proxynet.io:8000. O lado Node.js do mesmo trabalho está em Como usar proxy no Node.js.
Como o net/http decide qual proxy usar?
Toda requisição passa por um http.Transport, cujo campo Proxy é uma função que recebe a requisição e devolve a URL do proxy, ou nil para uma conexão direta. O esquema da URL escolhe o tipo de proxy: a documentação do Transport em net/http lista http, https, socks5 e socks5h, um esquema vazio significa http, e as credenciais da URL viram um cabeçalho Proxy-Authorization. O http.DefaultTransport, que está por trás do http.Get, define Proxy como http.ProxyFromEnvironment; um Transport que você mesmo constrói não usa proxy até que você defina um.
Depois, conforme o destino:
- O Transport se conecta ao proxy, não ao destino; se a porta faltar, ele usa 80 para
http, 443 parahttpse 1080 para SOCKS. - Destino
http://simples. A requisição vai ao proxy com a URL completa na linha de requisição, maisProxy-Authorizationem cada requisição. - Destino
https://. O Go enviaCONNECT host:443com o cabeçalho uma só vez; depois do200do proxy, o handshake TLS roda dentro do túnel e o proxy vê apenas o host e a porta. O método CONNECT é definido na RFC 9110. - Proxy
socks5://. O Go faz o handshake SOCKS, faz login se a URL tiver credenciais e envia o nome do host do destino, então o proxy resolve o DNS. - A conexão fica no pool para a próxima requisição ao mesmo destino pelo mesmo proxy, por keep-alive do HTTP/1.1 ou por HTTP/2, e uma conexão reutilizada não chama
Proxyde novo.
Como definir um proxy com http.ProxyURL?
O http.ProxyURL envolve uma URL fixa em uma função com a forma certa. Faça o parse do endereço uma vez, coloque-o em um Transport e entregue o Transport a um Client que também carregue um timeout:
package main
import (
"cmp"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
// O Gerador de endpoints do painel imprime esta string. Guarde-a em uma variável de ambiente, não no 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"
}Do lado do proxy, as duas diferem: um CONNECT para o destino HTTPS e um GET encaminhado com a URL completa para o destino HTTP, ambos com credenciais:
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 -> forwardDois hábitos: resp.Body.Close() depois de cada resposta, ou a conexão não consegue voltar ao pool, e um Timeout no Client, ou uma saída que para de responder segura uma goroutine para sempre. O User-Agent padrão é Go-http-client/1.1; defina o seu para que o destino consiga identificar o seu programa.
Como o Go lê HTTP_PROXY, HTTPS_PROXY e NO_PROXY?
O http.ProxyFromEnvironment lê as três variáveis, ou as formas em minúsculas delas, a cada requisição; as regras estão na documentação do pacote httpproxy, a implementação que o net/http usa:
HTTP_PROXYvale para destinoshttp://eHTTPS_PROXYpara destinoshttps://: quem decide é o esquema do destino, não o do proxy, então um endereço de proxyhttp://emHTTPS_PROXYé normal.- Um valor é uma URL completa ou um
host[:port]sem esquema, tratado comohttp; qualquer outra coisa faz a requisição falhar. - Se existirem a variável em maiúsculas e a em minúsculas, a de minúsculas vence.
NO_PROXYlista endereços IP, faixas CIDR como10.0.0.0/8e nomes de domínio, cada um com porta opcional. Um domínio corresponde a si mesmo e aos seus subdomínios; um nome que começa com ponto corresponde só aos subdomínios; um único*desliga o proxy.localhoste os endereços de loopback nunca passam pelo proxy.
Este programa pergunta ao ProxyFromEnvironment sobre cinco destinos sem 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())
}
}
}Com HTTPS_PROXY e HTTP_PROXY definidas e 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/ directSó com HTTP_PROXY definida, os destinos HTTPS vão direto, o motivo mais comum de um programa em Go "ignorar o 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() mascara a senha como xxxxx; use-o nos logs.
Uma regra não está na semântica das variáveis: o net/http lê o ambiente uma vez, na primeira requisição, e o guarda em cache pela vida inteira do processo, então um os.Setenv depois disso não tem efeito:
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000Para captar um ambiente alterado em tempo de execução, coloque em Transport.Proxy a função httpproxy.FromEnvironment().ProxyFunc(), que lê as variáveis a cada chamada.
Como o Go envia o usuário e a senha do proxy?
Quando a URL do proxy tem usuário e senha, o Go envia Proxy-Authorization: Basic … por conta própria. Para um destino http:// simples, o cabeçalho viaja com cada requisição, mesmo em uma conexão keep-alive: no nosso teste de novas tentativas, cada um dos quatro GET em um mesmo socket o carregava. Para um destino https://, ele é enviado uma vez, no CONNECT, e o túnel é reutilizado enquanto a conexão viver.
Caracteres como @, : ou / na senha quebram a URL se não forem codificados em percentual, então monte a URL por partes:
proxyURL := &url.URL{
Scheme: "http",
Host: "pr.proxynet.io:8000",
User: url.UserPassword("user", "p@ss:word"), // na rede vai codificada como p%40ss%3Aword
}Basic é o único esquema que o Go envia a um proxy; NTLM ou Kerberos precisa de um pacote de terceiros. O gateway de um provedor aceita Basic ou uma whitelist de IP, que não deixa nenhuma senha na URL e combina com um servidor de endereço fixo; os dois são comparados em Autenticação de proxy: user:pass ou whitelist de IP.
Como usar um proxy SOCKS5 em Go?
De duas formas. O Transport.Proxy aceita uma URL socks5:// e faz o handshake sozinho; o Go trata socks5 exatamente como socks5h, então o proxy resolve o nome do host do destino. O pacote golang.org/x/net/proxy, por sua vez, entrega o dialer SOCKS5 como um valor, que você encaixa em Transport.DialContext e também pode usar em conexões que não são 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:<a porta SOCKS5 que o Gerador de endpoints mostra>
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. O net/http trata socks5:// por conta própria. O proxy resolve o nome do 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 entrega o próprio dialer SOCKS5, utilizável em qualquer conexão 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"
}Nosso servidor SOCKS5 registrou os mesmos três passos para os dois clientes: o Go ofereceu os métodos "sem autenticação" e "usuário/senha", o servidor escolheu usuário/senha como definido na RFC 1929, e o pedido de conexão levava o nome de domínio, não um 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:443Pela via do dialer, deixe Transport.Proxy vazio; o dialer já passa pelo proxy. SOCKS5 transporta TCP e UDP, mas o net/http só abre conexões TCP. Os Proxies SOCKS5 da Proxynet rodam em uma porta própria ao lado da porta HTTP, com o mesmo usuário e a mesma senha.
Qual método serve para cada situação?
| Método | Onde entra | Alcance | Observação |
|---|---|---|---|
http.ProxyURL(u) | Transport.Proxy | aquele Transport | um proxy fixo |
http.ProxyFromEnvironment | Transport.Proxy (padrão no DefaultTransport) | o processo | lido uma vez; localhost fica de fora |
sua própria func(*http.Request) | Transport.Proxy | por requisição | rotação, regras por host |
URL socks5:// | qualquer uma das três acima | aquele Transport | DNS resolvido no proxy |
dialer do x/net/proxy | Transport.DialContext | aquele Transport | também TCP puro; deixe Proxy vazio |
Como usar um proxy diferente a cada requisição?
O Transport.Proxy é uma função comum, então pode devolver uma URL diferente a cada chamada. Esta escolhe em rodízio (round-robin) de uma lista e imprime a escolha:
var next atomic.Uint64
transport := &http.Transport{
// Rodízio sobre a lista. Chamada quando o Transport precisa de uma conexão para uma requisição.
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, // uma conexão nova, e portanto uma escolha de proxy nova, a cada requisição
}A última linha é a que importa. Enviamos quatro GET a https://httpbin.org/ip, que negocia HTTP/2, com dois proxies na lista, e contamos chamadas e túneis:
| Configuração do Transport | Função Proxy chamada | Túneis abertos |
|---|---|---|
| Padrões (HTTP/2 negociado) | 1 | 1, só o primeiro proxy |
Só HTTP/1.1 (mapa TLSNextProto vazio) | 4 | 2, um por proxy, reutilizados |
DisableKeepAlives: true | 4 | 4, alternando |
Com os padrões, o Go abriu uma conexão HTTP/2 pelo primeiro proxy e multiplexou as quatro requisições nela; a função nunca foi consultada de novo. Com keep-alive do HTTP/1.1, ela foi consultada a cada vez, mas as requisições três e quatro reutilizaram a conexão ociosa com o seu proxy. Só DisableKeepAlives, ou req.Close = true, dá um túnel por requisição. Um gateway rotativo segue a mesma regra, porque o IP de saída é escolhido quando o túnel abre: o gateway dos Proxies residenciais troca a saída a cada túnel novo através de um único endereço, e um parâmetro de sessão no nome de usuário mantém uma saída por 1 a 60 minutos para fluxos com login.
Timeouts, limites de conexão e TLS
http.Client.Timeout é a única configuração que nunca deve ficar em zero: ela cobre a requisição inteira, da conexão ao último byte. Os ajustes finos ficam no Transport: DialContext com um net.Dialer{Timeout: …} limita a conexão ao proxy, TLSHandshakeTimeout o handshake dentro do túnel, ResponseHeaderTimeout a espera pelos cabeçalhos; http.NewRequestWithContext com um prazo faz o mesmo para uma única requisição.
Três campos decidem quantas conexões você abre. MaxConnsPerHost limita as conexões por host e faz as demais esperarem, um limitador de taxa barato por destino. MaxIdleConnsPerHost tem 2 como padrão; com mais workers, as conexões concluídas são fechadas em vez de voltarem ao pool, então defina-o igual ao número de workers. IdleConnTimeout fecha conexões que ficaram sem uso.
Em TLS, nunca recorra a InsecureSkipVerify. x509: certificate signed by unknown authority atrás de um proxy significa que um proxy corporativo inspeciona o TLS com a própria autoridade certificadora; adicione essa CA a tls.Config.RootCAs. O gateway de um provedor nunca toca no TLS de dentro do túnel, então ali o erro aponta para o certificado do destino.
Um exemplo completo: novas tentativas, backoff e limite de concorrência
O downloader abaixo combina as peças: os timeouts e os limites de pool acima, novas tentativas em erros de rede, 429 e 5xx com backoff exponencial e jitter, Retry-After respeitado, nenhuma nova tentativa em um 407 e no máximo quatro requisições por vez:
// fetch.go - faz GET em uma lista de URLs por um proxy com timeouts, novas tentativas, backoff e limite de concorrência.
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 // primeira tentativa + 3 novas tentativas
maxInFlight = 4 // requisições rodando ao mesmo tempo
)
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, // a requisição inteira: conexão, cabeçalhos e corpo
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,
// Um 407 no túnel CONNECT vira um erro tipado em vez 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 ... com teto de 30 s, mais até 1 s de jitter para que os workers não tentem de novo em sincronia.
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 faz GET em uma URL. Erros de rede, 429 e 5xx são tentados de novo; um 407 não, porque as
// credenciais estão erradas e outra tentativa não resolve isso.
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 simples: o 407 é uma resposta
return body, resp.StatusCode, errProxyAuth
}
if !retryStatus[resp.StatusCode] {
return body, resp.StatusCode, err // 200, 404, 301 ... quem chamou decide
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
if attempt == maxAttempts {
break
}
// O site pediu para irmos mais devagar. O Retry-After dele vence a nossa agenda.
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{}{} // bloqueia enquanto maxInFlight requisições estão em andamento
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 URLs, quatro workers, um endereço que sempre responde 503 para exercitar o caminho das novas tentativas:
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 failedTrês decisões de projeto. OnProxyConnectResponse roda quando o proxy responde ao CONNECT, antes de o Go transformar um status diferente de 200 em uma simples string de erro; um erro tipado ali permite que errors.Is interrompa o laço de novas tentativas, e com uma senha errada o programa imprimiu fail … -> proxy rejected the credentials (407) para um destino HTTPS e um HTTP simples sem uma única nova tentativa. O conjunto de status que recebem nova tentativa é pequeno de propósito: um 404 ou 403 é devolvido como está, porque outra saída não muda o que a página é. Contra um servidor local que enviava Retry-After: 2, o laço dormiu exatamente 2 s entre as tentativas em vez de seguir a própria agenda.
Como verificar se a requisição realmente usa o proxy?
Pergunte a um serviço de eco qual endereço ele vê, uma vez com Proxy: nil e outra com o proxy. O nil é deliberado: um Transport sem valor em Proxy nunca lê o ambiente, então a primeira linha é o seu próprio endereço mesmo com 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 faz GET em https://api.ipify.org?format=json e devolve o campo "ip"
fmt.Println("with proxy: ", exitIP(proxied))Com um gateway real, a segunda linha mostra o endereço da saída. Com o nosso proxy de teste, as duas linhas imprimiram o mesmo endereço, porque o proxy roda na mesma máquina; a prova foi a linha CONNECT api.ipify.org:443 no log dele. Duas linhas idênticas com um proxy real costumam significar HTTP_PROXY sem HTTPS_PROXY, um destino em NO_PROXY ou um Transport criado sem valor em Proxy.
E o Colly?
O Colly, o framework de crawling mais usado em Go, é construído sobre o http.Transport, então tudo acima se aplica. O pacote proxy dele traz um RoundRobinProxySwitcher que recebe URLs http://, https:// ou socks5:// e devolve uma colly.ProxyFunc, a mesma assinatura de Transport.Proxy; você o conecta com c.SetProxyFunc(rp), como no exemplo do proxy switcher da documentação do Colly. O switcher é chamado por requisição, mas a regra de reutilização de conexões da seção de rotação continua decidindo se uma requisição abre um túnel novo.
Erros comuns e o que significam
Mensagens como o Go 1.26.6 as imprimiu nos nossos testes, com o host de exemplo:
Get "https://httpbin.org/ip": Proxy Authentication Requiredpara um destino HTTPS, ou status407como resposta normal para um destino HTTP simples:user:passerrado, caracteres especiais sem codificação ou um IP fora da whitelist. As causas estão reunidas em Como corrigir HTTP 407 Proxy Authentication Required.proxyconnect tcp: dial tcp pr.proxynet.io:8000: …mais o texto de conexão recusada do sistema operacional (connect: connection refusedno Linux e no macOS,connectex:e uma frase localizada no Windows): nada escuta ali.proxyconnectdiz que a falha foi no caminho até o proxy, não até o destino.socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: uma URLsocks5://apontava para a porta HTTP; 72 é o valor do byte doHdeHTTP/1.1.socks connect tcp …: username/password authentication failed: o servidor SOCKS5 recusou as credenciais.unexpected EOF: uma URLhttp://na porta SOCKS5, que fechou a conexão ao receber oCONNECT; um esquema desconhecido comosocks4://é tratado comohttpe termina do mesmo jeito.proxyconnect tcp: tls: first record does not look like a TLS handshake: um esquema de proxyhttps://em uma porta HTTP simples; usehttp://a menos que o provedor diga que a porta roda TLS.- O proxy simplesmente não é usado:
HTTP_PROXYsemHTTPS_PROXY, um destino que élocalhostou está emNO_PROXY, variáveis definidas depois da primeira requisição ou um Transport construído sem valor emProxy.
Onde essa configuração é usada
- Verificações de preço e disponibilidade a partir de vários países: um parâmetro de país no nome de usuário do proxy e rotação por requisição para páginas de produto independentes.
- Clientes de API atrás de um endereço autorizado, quando um parceiro colocou um IP estático na whitelist e o serviço precisa sair por ele.
- Monitoramento de fora da sua própria rede, para que uma verificação de saúde veja o site como um visitante de outra região o vê.
- Workers em uma rede corporativa que só chegam à internet pelo proxy da empresa.
Erros de configuração mais comuns
- Definir só
HTTP_PROXY. Os destinos HTTPS vão direto; defina tambémHTTPS_PROXY. - Mudar o ambiente depois da primeira requisição. O net/http o guarda em cache; reinicie o processo ou use o
httpproxy. - Esperar rotação com os keep-alives ligados. Uma conexão reutilizada nunca chama
Proxyde novo. - Esquecer o
resp.Body.Close(). As conexões nunca voltam ao pool. - Nenhum
Client.Timeout. Uma saída silenciosa bloqueia uma goroutine para sempre. - Colar uma senha com
@ou:na string da URL. Monte-a comurl.UserPassword. - Definir ao mesmo tempo
Transport.Proxye umDialContextSOCKS5. Escolha um. - Tentar de novo um
407. A segunda tentativa envia as mesmas credenciais erradas.
Qual abordagem escolher?
| Sua situação | Recomendação |
|---|---|
| Um proxy, sem mudar o código | HTTPS_PROXY e HTTP_PROXY, NO_PROXY para os hosts internos |
| Um proxy, definido no código | Transport{Proxy: http.ProxyURL(u)} mais Client.Timeout |
| Uma saída nova a cada requisição | Sua própria função Proxy ou um gateway rotativo, DisableKeepAlives: true |
| A mesma saída para um fluxo com login | Um parâmetro de sessão no nome de usuário, keep-alives ligados |
| SOCKS5 com DNS no proxy | socks5://user:pass@host:port em Transport.Proxy |
| SOCKS5 para uma conexão que não é HTTP | Dialer do golang.org/x/net/proxy |
| Muitos workers em um mesmo site | MaxConnsPerHost e MaxIdleConnsPerHost = número de workers |
| Proxy corporativo com CA própria | A CA em tls.Config.RootCAs, nunca InsecureSkipVerify |
Perguntas frequentes
O http.Get em Go usa proxy automaticamente?
Sim, quando o ambiente manda: o http.Get usa o http.DefaultClient, cujo Transport define Proxy como http.ProxyFromEnvironment. Um Transport que você mesmo cria começa sem proxy.
Isso é o mesmo que GOPROXY?
Não. GOPROXY diz ao comando go de onde baixar os módulos; não tem nada a ver com as requisições que o seu programa envia em tempo de execução.
Como definir um proxy só para uma requisição?
Dê a essa requisição um Client e um Transport próprios, ou escreva uma função Proxy que devolva nil para as requisições que devem ir direto. Uma conexão no pool para o mesmo host pode já existir; req.Close = true força uma nova.
O Go suporta socks5h?
Sim, e socks5 se comporta do mesmo jeito: o Go sempre envia o nome do host ao proxy, que o resolve; o net/http não tem um modo SOCKS5 com resolução local.
Por que localhost nunca passa pelo proxy?
O ProxyFromEnvironment não devolve proxy para localhost nem para endereços de loopback, digam as variáveis o que disserem; para passar essas requisições pelo proxy, use http.ProxyURL.
Como ver qual proxy uma requisição usou?
Envolva a função Proxy e registre u.Redacted(), ou defina Transport.OnProxyConnectResponse, que recebe a URL do proxy e a resposta ao CONNECT. Com os keep-alives ligados, a escolha só é registrada quando uma conexão nova abre.
Em resumo
Em Go, o proxy é uma função do http.Transport: http.ProxyURL para um endereço fixo, http.ProxyFromEnvironment para as variáveis padrão ou a sua própria função para escolher por requisição. Coloque user:pass na URL e o Go envia Proxy-Authorization por conta própria; use socks5:// para SOCKS5; defina Client.Timeout e os limites do pool; tente de novo 429 e 5xx com backoff, mas nunca um 407; desligue os keep-alives quando cada requisição precisar de uma saída nova. Os endpoints residenciais, ISP estáticos, de datacenter e SOCKS5 que funcionam com essas configurações estão na página de proxies da Proxynet.




