Proxy HTTP em Golang: como usar um proxy com net/http

Publicado:

16 min de leitura

Acar Diveroli
Autor: Acar Diveroli
Três paradas numa rota: cliente net/http, grande parada proxy azul com ícone de túnel e um destino que responde 200 OK

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:

  1. O Transport se conecta ao proxy, não ao destino; se a porta faltar, ele usa 80 para http, 443 para https e 1080 para SOCKS.
  2. Destino http:// simples. A requisição vai ao proxy com a URL completa na linha de requisição, mais Proxy-Authorization em cada requisição.
  3. Destino https://. O Go envia CONNECT host:443 com o cabeçalho uma só vez; depois do 200 do 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.
  4. 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.
  5. 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 Proxy de 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:

go
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)
	}
}
text
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:

text
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 -> forward

Dois 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_PROXY vale para destinos http:// e HTTPS_PROXY para destinos https://: quem decide é o esquema do destino, não o do proxy, então um endereço de proxy http:// em HTTPS_PROXY é normal.
  • Um valor é uma URL completa ou um host[:port] sem esquema, tratado como http; 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_PROXY lista endereços IP, faixas CIDR como 10.0.0.0/8 e 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.
  • localhost e os endereços de loopback nunca passam pelo proxy.

Este programa pergunta ao ProxyFromEnvironment sobre cinco destinos sem enviar nada:

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

text
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/               direct

Só com HTTP_PROXY definida, os destinos HTTPS vão direto, o motivo mais comum de um programa em Go "ignorar o proxy":

text
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/               direct

Redacted() 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:

text
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000

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

go
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.

go
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)
}
text
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:

text
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:443

Pela 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étodoOnde entraAlcanceObservação
http.ProxyURL(u)Transport.Proxyaquele Transportum proxy fixo
http.ProxyFromEnvironmentTransport.Proxy (padrão no DefaultTransport)o processolido uma vez; localhost fica de fora
sua própria func(*http.Request)Transport.Proxypor requisiçãorotação, regras por host
URL socks5://qualquer uma das três acimaaquele TransportDNS resolvido no proxy
dialer do x/net/proxyTransport.DialContextaquele Transporttambé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:

go
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 TransportFunção Proxy chamadaTúneis abertos
Padrões (HTTP/2 negociado)11, só o primeiro proxy
Só HTTP/1.1 (mapa TLSNextProto vazio)42, um por proxy, reutilizados
DisableKeepAlives: true44, 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:

go
// 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:

text
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 failed

Trê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.

go
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 Required para um destino HTTPS, ou status 407 como resposta normal para um destino HTTP simples: user:pass errado, 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 refused no Linux e no macOS, connectex: e uma frase localizada no Windows): nada escuta ali. proxyconnect diz 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 URL socks5:// apontava para a porta HTTP; 72 é o valor do byte do H de HTTP/1.1.
  • socks connect tcp …: username/password authentication failed: o servidor SOCKS5 recusou as credenciais.
  • unexpected EOF: uma URL http:// na porta SOCKS5, que fechou a conexão ao receber o CONNECT; um esquema desconhecido como socks4:// é tratado como http e termina do mesmo jeito.
  • proxyconnect tcp: tls: first record does not look like a TLS handshake: um esquema de proxy https:// em uma porta HTTP simples; use http:// a menos que o provedor diga que a porta roda TLS.
  • O proxy simplesmente não é usado: HTTP_PROXY sem HTTPS_PROXY, um destino que é localhost ou está em NO_PROXY, variáveis definidas depois da primeira requisição ou um Transport construído sem valor em Proxy.

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ém HTTPS_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 Proxy de 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 com url.UserPassword.
  • Definir ao mesmo tempo Transport.Proxy e um DialContext SOCKS5. Escolha um.
  • Tentar de novo um 407. A segunda tentativa envia as mesmas credenciais erradas.

Qual abordagem escolher?

Sua situaçãoRecomendação
Um proxy, sem mudar o códigoHTTPS_PROXY e HTTP_PROXY, NO_PROXY para os hosts internos
Um proxy, definido no códigoTransport{Proxy: http.ProxyURL(u)} mais Client.Timeout
Uma saída nova a cada requisiçãoSua própria função Proxy ou um gateway rotativo, DisableKeepAlives: true
A mesma saída para um fluxo com loginUm parâmetro de sessão no nome de usuário, keep-alives ligados
SOCKS5 com DNS no proxysocks5://user:pass@host:port em Transport.Proxy
SOCKS5 para uma conexão que não é HTTPDialer do golang.org/x/net/proxy
Muitos workers em um mesmo siteMaxConnsPerHost e MaxIdleConnsPerHost = número de workers
Proxy corporativo com CA própriaA 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.