Your Go service has to send its requests through a proxy: an exit in another country, a fixed IP a partner has whitelisted, or a pool so that one address does not carry every request. Go needs no extra package for this; net/http has proxy support built in, SOCKS5 and the standard environment variables included. The surprises are in the defaults: http.Get already reads HTTPS_PROXY, localhost is never proxied, the variables are read once per process, and an HTTP/2 connection can quietly skip the proxy function you wrote for rotation.
This guide covers http.ProxyURL, ProxyFromEnvironment and the NO_PROXY rules, the Proxy-Authorization header, SOCKS5 in both of Go's forms, a different proxy per request, timeouts, retries, the exact error texts and a complete fetcher. Every snippet ran on Go 1.26.6 (golang.org/x/net v0.62.0) against a local HTTP proxy that requires user:pass and a local SOCKS5 server; outputs and log lines are real, only the proxy host was replaced by pr.proxynet.io:8000. The Node.js side of the same job is in Using a Proxy in Node.js.
How does net/http decide which proxy to use?
Every request goes through an http.Transport, whose Proxy field is a function that receives the request and returns the proxy URL, or nil for a direct connection. The URL's scheme picks the proxy type: the net/http Transport documentation lists http, https, socks5 and socks5h, an empty scheme means http, and credentials in the URL become a Proxy-Authorization header. http.DefaultTransport, behind http.Get, sets Proxy to http.ProxyFromEnvironment; a Transport you build yourself uses no proxy until you set one.
Then, depending on the target:
- The Transport dials the proxy, not the target; a missing port falls back to 80 for
http, 443 forhttpsand 1080 for SOCKS. - Plain
http://target. The request goes to the proxy with the full URL in the request line, plusProxy-Authorizationon every request. https://target. Go sendsCONNECT host:443with the header once; after the proxy's200, the TLS handshake runs inside the tunnel and the proxy sees only host and port. The CONNECT method is defined in RFC 9110.socks5://proxy. Go runs the SOCKS handshake, logs in if the URL has credentials, and sends the target's host name, so the proxy resolves DNS.- The connection is pooled for the next request to the same target through the same proxy, over HTTP/1.1 keep-alive or HTTP/2, and a reused connection does not call
Proxyagain.
How do you set a proxy with http.ProxyURL?
http.ProxyURL wraps a fixed URL in a function of the right shape. Parse the address once, put it in a Transport, and give the Transport to a Client that also carries a timeout:
package main
import (
"cmp"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
// The endpoint builder in the panel prints this string. Keep it in an environment variable, not in 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"
}On the proxy side the two differ: one CONNECT for the HTTPS target, one forwarded GET with the full URL for the HTTP target, both with credentials:
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 -> forwardTwo habits: resp.Body.Close() after every response, or the connection cannot return to the pool, and a Timeout on the Client, or an exit that stops answering holds a goroutine forever. The default User-Agent is Go-http-client/1.1; set your own so the target can identify your program.
How does Go read HTTP_PROXY, HTTPS_PROXY and NO_PROXY?
http.ProxyFromEnvironment reads the three variables, or their lowercase forms, per request; the rules are in the httpproxy package documentation, the implementation net/http uses:
HTTP_PROXYapplies tohttp://targets,HTTPS_PROXYtohttps://targets: the target's scheme decides, not the proxy's, so anhttp://proxy address inHTTPS_PROXYis normal.- A value is a full URL or a bare
host[:port], treated ashttp; anything else makes the request fail. - If both the uppercase and the lowercase variable exist, the lowercase one wins.
NO_PROXYlists IP addresses, CIDR ranges such as10.0.0.0/8and domain names, each optionally with a port. A domain matches itself and its subdomains; a name that starts with a dot matches subdomains only; a single*disables proxying.localhostand loopback addresses are never proxied.
This program asks ProxyFromEnvironment about five targets without sending anything:
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())
}
}
}With HTTPS_PROXY and HTTP_PROXY both set and 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/ directWith only HTTP_PROXY set, the HTTPS targets go direct, the most common reason a Go program "ignores the 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() masks the password as xxxxx; use it in logs.
One rule is not in the variable semantics: net/http reads the environment once, on the first request, and caches it for the life of the process, so os.Setenv after that has no effect:
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000To pick up a changed environment at runtime, put httpproxy.FromEnvironment().ProxyFunc(), which reads the variables on every call, into Transport.Proxy.
How does Go send the proxy username and password?
When the proxy URL has a user and a password, Go sends Proxy-Authorization: Basic … on its own. For a plain http:// target the header travels with every request, even on a kept-alive connection: in our retry test, four GETs on one socket each carried it. For an https:// target it is sent once, in the CONNECT, and the tunnel is reused for as long as the connection lives.
Characters such as @, : or / in the password break the URL unless they are percent-encoded, so build the URL from parts:
proxyURL := &url.URL{
Scheme: "http",
Host: "pr.proxynet.io:8000",
User: url.UserPassword("user", "p@ss:word"), // encoded as p%40ss%3Aword on the wire
}Basic is the only scheme Go sends to a proxy; NTLM or Kerberos needs a third-party package. A provider's gateway takes Basic or an IP whitelist, which leaves no password in the URL and suits a server with a fixed address; the two are compared in Proxy Authentication: User:Pass vs IP Whitelist.
How do you use a SOCKS5 proxy in Go?
Two ways. Transport.Proxy accepts a socks5:// URL and performs the handshake itself; Go treats socks5 exactly like socks5h, so the proxy resolves the target's host name. The golang.org/x/net/proxy package instead hands you the SOCKS5 dialer as a value, which you plug into Transport.DialContext and can also use for connections that are not 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:<the SOCKS5 port the endpoint builder shows>
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 handles socks5:// on its own. The proxy resolves the host name.
viaTransport := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
Timeout: 20 * time.Second,
}
show("Transport.Proxy", viaTransport)
// 2. golang.org/x/net/proxy gives you the SOCKS5 dialer itself, usable for any TCP connection.
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"
}Our SOCKS5 server logged the same three steps for both clients: Go offered the "no authentication" and "username/password" methods, the server chose username/password as defined in RFC 1929, and the connect request carried the domain name, not an 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:443With the dialer route, leave Transport.Proxy empty; the dialer already goes through the proxy. SOCKS5 carries TCP and UDP, but net/http only opens TCP connections. Proxynet's SOCKS5 Proxy runs on its own port next to the HTTP port, with the same username and password.
Which method fits which situation?
| Method | Where it goes | Scope | Note |
|---|---|---|---|
http.ProxyURL(u) | Transport.Proxy | that Transport | one fixed proxy |
http.ProxyFromEnvironment | Transport.Proxy (default in DefaultTransport) | the process | read once; localhost bypassed |
your own func(*http.Request) | Transport.Proxy | per request | rotation, per-host rules |
socks5:// URL | any of the three above | that Transport | DNS resolved at the proxy |
x/net/proxy dialer | Transport.DialContext | that Transport | also raw TCP; keep Proxy empty |
How do you use a different proxy for each request?
Transport.Proxy is an ordinary function, so it can return a different URL on every call. This one picks round-robin from a list and prints its choice:
var next atomic.Uint64
transport := &http.Transport{
// Round-robin over the list. Called when the Transport needs a connection for a request.
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, // a new connection, and therefore a new proxy choice, for every request
}The last line matters. We sent four GETs to https://httpbin.org/ip, which negotiates HTTP/2, with two proxies in the list, and counted calls and tunnels:
| Transport settings | Proxy function called | Tunnels opened |
|---|---|---|
| Defaults (HTTP/2 negotiated) | 1 | 1, first proxy only |
HTTP/1.1 only (empty TLSNextProto map) | 4 | 2, one per proxy, reused |
DisableKeepAlives: true | 4 | 4, alternating |
With the defaults, Go opened one HTTP/2 connection through the first proxy and multiplexed all four requests over it; the function was never asked again. With HTTP/1.1 keep-alive it was asked each time, but requests three and four reused the idle connection to their proxy. Only DisableKeepAlives, or req.Close = true, gives one tunnel per request. A rotating gateway follows the same rule, because the exit IP is chosen when the tunnel opens: a Residential Proxy gateway rotates the exit for every new tunnel through one address, and a session parameter in the username holds an exit for 1 to 60 minutes for login flows.
Timeouts, connection limits and TLS
http.Client.Timeout is the one setting never to leave at zero: it covers the whole request, dial to last byte. The finer knobs live on the Transport: DialContext with a net.Dialer{Timeout: …} bounds the connection to the proxy, TLSHandshakeTimeout the handshake inside the tunnel, ResponseHeaderTimeout the wait for headers; http.NewRequestWithContext with a deadline does the same for one request.
Three fields decide how many connections you open. MaxConnsPerHost caps connections per host and makes further dials wait, a cheap per-target rate limiter. MaxIdleConnsPerHost defaults to 2; with more workers, finished connections are closed instead of pooled, so set it to the worker count. IdleConnTimeout closes connections that sat unused.
For TLS, never reach for InsecureSkipVerify. x509: certificate signed by unknown authority behind a proxy means a corporate proxy inspects TLS with its own certificate authority; add that CA to tls.Config.RootCAs. A provider's gateway never touches the TLS inside the tunnel, so there the error points at the target's certificate.
A complete example: retries, backoff and a concurrency cap
The fetcher below combines the pieces: the timeouts and pool limits above, retries for network errors, 429 and 5xx with exponential backoff and jitter, Retry-After honoured, no retry on a 407, and at most four requests at once:
// fetch.go - GET a list of URLs through a proxy with timeouts, retries, backoff and a concurrency cap.
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 // first try + 3 retries
maxInFlight = 4 // requests running at the same time
)
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, // the whole request: connect, headers and body
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,
// A 407 on the CONNECT tunnel becomes a typed error instead of "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 ... capped at 30 s, plus up to 1 s of jitter so workers do not retry in lockstep.
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 GETs one URL. Network errors, 429 and 5xx are retried; a 407 is not, because the
// credentials are wrong and another attempt cannot fix that.
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 { // plain-http target: the 407 is a response
return body, resp.StatusCode, errProxyAuth
}
if !retryStatus[resp.StatusCode] {
return body, resp.StatusCode, err // 200, 404, 301 ... the caller decides
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
if attempt == maxAttempts {
break
}
// The site asked us to slow down. Its own Retry-After wins over our schedule.
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{}{} // blocks while maxInFlight requests are running
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())
}Five URLs, four workers, one address that always answers 503 to exercise the retry path:
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 failedThree design points. OnProxyConnectResponse runs when the proxy answers the CONNECT, before Go turns a non-200 status into a plain error string; a typed error there lets errors.Is stop the retry loop, and with a wrong password the program printed fail … -> proxy rejected the credentials (407) for an HTTPS and a plain HTTP target without a single retry. The retry set is small on purpose: a 404 or 403 is returned as-is, because another exit does not change what the page is. Against a local server that sent Retry-After: 2, the loop slept exactly 2 s between attempts instead of its own schedule.
How do you check that the request really uses the proxy?
Ask an echo service which address it sees, once with Proxy: nil and once with the proxy. The nil is deliberate: a Transport without a Proxy value never reads the environment, so the first line is your own address even with HTTPS_PROXY set.
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 GETs https://api.ipify.org?format=json and returns the "ip" field
fmt.Println("with proxy: ", exitIP(proxied))With a real gateway the second line shows the exit's address. With our test proxy both lines printed the same address, because the proxy runs on the same machine; the proof was the CONNECT api.ipify.org:443 line in its log. Two identical lines with a real proxy usually mean HTTP_PROXY without HTTPS_PROXY, a target in NO_PROXY, or a Transport created without a Proxy value.
What about Colly?
Colly, the most used Go crawling framework, builds on http.Transport, so everything above applies. Its proxy package ships a RoundRobinProxySwitcher that takes http://, https:// or socks5:// URLs and returns a colly.ProxyFunc, the same signature as Transport.Proxy; you attach it with c.SetProxyFunc(rp), as in the proxy switcher example in Colly's documentation. The switcher is called per request, but the connection-reuse rule from the rotation section still decides whether a request opens a new tunnel.
Common errors and what they mean
Messages as Go 1.26.6 printed them in our tests, with the placeholder host:
Get "https://httpbin.org/ip": Proxy Authentication Requiredfor an HTTPS target, or status407as a normal response for a plain HTTP target: wronguser:pass, unencoded special characters, or an IP not on the whitelist. The causes are collected in HTTP 407 Proxy Authentication Required.proxyconnect tcp: dial tcp pr.proxynet.io:8000: …plus the operating system's refused-connection text (connect: connection refusedon Linux and macOS,connectex:and a localized sentence on Windows): nothing listens there.proxyconnectsays the failure was on the way to the proxy, not to the target.socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: asocks5://URL pointed at the HTTP port; 72 is the byte value of theHinHTTP/1.1.socks connect tcp …: username/password authentication failed: the SOCKS5 server rejected the credentials.unexpected EOF: anhttp://URL at the SOCKS5 port, which closed the connection on theCONNECT; an unknown scheme such assocks4://is treated likehttpand ends the same way.proxyconnect tcp: tls: first record does not look like a TLS handshake: anhttps://proxy scheme at a plain HTTP port; usehttp://unless the provider says the port runs TLS.- The proxy is simply not used:
HTTP_PROXYwithoutHTTPS_PROXY, a target that islocalhostor inNO_PROXY, variables set after the first request, or a Transport built without aProxyvalue.
Where this setup is used
- Price and availability checks from several countries: a country parameter in the proxy username and per-request rotation for independent product pages.
- API clients behind an allowlisted address, where a partner has whitelisted one static IP and the service must leave through it.
- Monitoring from outside your own network, so a health check sees the site the way a visitor in another region does.
- Workers in a corporate network that reach the internet only through the company proxy.
Common mistakes
- Setting
HTTP_PROXYonly. HTTPS targets go direct; setHTTPS_PROXYas well. - Changing the environment after the first request. net/http caches it; restart the process or use
httpproxy. - Expecting rotation with keep-alives on. A reused connection never calls
Proxyagain. - Forgetting
resp.Body.Close(). Connections never return to the pool. - No
Client.Timeout. A silent exit blocks a goroutine forever. - Pasting a password with
@or:into the URL string. Build it withurl.UserPassword. - Setting both
Transport.Proxyand a SOCKS5DialContext. Pick one. - Retrying a
407. The second attempt sends the same wrong credentials.
Which approach should you choose?
| Your situation | Recommendation |
|---|---|
| One proxy, no code changes | HTTPS_PROXY and HTTP_PROXY, NO_PROXY for internal hosts |
| One proxy, set in code | Transport{Proxy: http.ProxyURL(u)} plus Client.Timeout |
| A new exit for every request | Your own Proxy function or a rotating gateway, DisableKeepAlives: true |
| The same exit for a login flow | A session parameter in the username, keep-alives on |
| SOCKS5 with DNS at the proxy | socks5://user:pass@host:port in Transport.Proxy |
| SOCKS5 for a non-HTTP connection | golang.org/x/net/proxy dialer |
| Many workers on one site | MaxConnsPerHost and MaxIdleConnsPerHost = worker count |
| Corporate proxy with its own CA | The CA in tls.Config.RootCAs, never InsecureSkipVerify |
Frequently asked questions
Does http.Get in Go use a proxy automatically?
Yes, when the environment says so: http.Get uses http.DefaultClient, whose Transport sets Proxy to http.ProxyFromEnvironment. A Transport you create yourself starts with no proxy.
Is this the same as GOPROXY?
No. GOPROXY tells the go command where to download modules from; it has nothing to do with the requests your program sends at run time.
How do I set a proxy for one request only?
Give that request its own Client and Transport, or write a Proxy function that returns nil for requests that should go direct. A pooled connection to the same host may already exist; req.Close = true forces a fresh one.
Does Go support socks5h?
Yes, and socks5 behaves the same way: Go always sends the host name to the proxy, which resolves it; net/http has no local-resolution SOCKS5 mode.
Why is localhost never proxied?
ProxyFromEnvironment returns no proxy for localhost and loopback addresses, whatever the variables say; to proxy such requests, use http.ProxyURL instead.
How can I see which proxy a request used?
Wrap the Proxy function and log u.Redacted(), or set Transport.OnProxyConnectResponse, which receives the proxy URL and the CONNECT response. With keep-alives on, a choice is logged only when a new connection opens.
Summary
In Go the proxy is a function on http.Transport: http.ProxyURL for a fixed address, http.ProxyFromEnvironment for the standard variables, or your own function for per-request choice. Put user:pass in the URL and Go sends Proxy-Authorization on its own; use socks5:// for SOCKS5; set Client.Timeout and the pool limits; retry 429 and 5xx with backoff but never a 407; turn keep-alives off when each request needs a new exit. Residential, static ISP, datacenter and SOCKS5 endpoints that work with these settings are on the Proxynet proxy page.




