---
title: "Proxy HTTP em Golang: como usar um proxy com net/http"
description: "Use http.ProxyURL ou ProxyFromEnvironment em http.Transport.Proxy e ponha user:pass na URL do proxy. SOCKS5, timeouts, novas tentativas e rotação no Go 1.26."
url: https://proxynet.io/pt-br/blog/golang-proxy
date: 2026-10-28
author: "Acar Diveroli"
category: "Tutoriais, Integração"
lang: pt-BR
---

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

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](/pt-br/blog/nodejs-proxy).

> **Nota: Resposta rápida**
>
> Crie um `http.Transport` com `Proxy: http.ProxyURL(u)`, em que `u` é o endereço `http://user:pass@pr.proxynet.io:8000` já parseado, e entregue-o a um `http.Client` com `Timeout`; o Go envia o usuário e a senha como `Proxy-Authorization` por conta própria. Para SOCKS5, use o esquema `socks5://` na mesma URL. O `http.Get` usa `http.ProxyFromEnvironment`, então um programa comum já respeita `HTTPS_PROXY`, `HTTP_PROXY` e `NO_PROXY`. Para um proxy diferente a cada requisição, escreva a sua própria função `Proxy` e desligue os keep-alives, porque uma conexão reutilizada nunca a consulta de novo.

## 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](https://pkg.go.dev/net/http#Transport) 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](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.6) é 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](https://pkg.go.dev/golang.org/x/net/http/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](/pt-br/blog/proxy-authentication-methods).

## 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](https://www.rfc-editor.org/rfc/rfc1929), 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](https://proxynet.io/pt-br/socks5-proxy) 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:

```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 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](https://proxynet.io/pt-br/residential-proxy) 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](https://go-colly.org/docs/examples/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](/pt-br/blog/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çã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](/pt-br/proxy).
