A Golang HTTP proxy is set on the http.Transport: put http.ProxyURL(u) in its Proxy field, where u is the proxy URL with the username and password in its userinfo. Go sends them as Proxy-Authorization, opens a CONNECT tunnel for HTTPS sites and does TLS with the target itself. No third-party package is needed, SOCKS5 included.

proxyURL := &url.URL{
	Scheme: "http",
	User:   url.UserPassword(os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASS")),
	Host:   net.JoinHostPort(os.Getenv("PROXY_HOST"), os.Getenv("PROXY_PORT")),
}
client := &http.Client{
	Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
	Timeout:   30 * time.Second,
}
resp, err := client.Get("https://api.ipify.org?format=json")

Every snippet on this page ran on 2026-09-30 with Go 1.26.6 (the stdlib parts also on Go 1.23) and golang.org/x/net v0.59.0, against two local authenticating HTTP proxies and a SOCKS5 proxy that enforces username and password and logs what it is asked to connect to.

Before you start: copy your proxy details

  1. Open your order in the dashboard at https://app.proxyhive.io.
  2. Copy HOST, PORT, USERNAME and PASSWORD. Every ISP or datacenter IP is its own endpoint, and the order lists separate HTTP, HTTPS and SOCKS5 ports for each one. Use the HTTP port with an http:// proxy URL.
  3. Export them so they stay out of source control:
export PROXY_HOST=HOST PROXY_PORT=PORT PROXY_USER=USERNAME PROXY_PASS=PASSWORD

Need an address to try this on? Static ISP proxies start at one IP.

Golang HTTP proxy with authentication: http.ProxyURL

The snippet above is the whole setup. Two details are worth knowing.

Build the URL with url.URL and url.UserPassword rather than url.Parse on a string. UserPassword escapes @, : and / in the password for you, and string parsing fails loudly on placeholders: url.Parse("http://USERNAME:PASSWORD@HOST:PORT") returns invalid port ":PORT" after host, which is a common first-run surprise.

The scheme in the proxy URL is how Go talks to the proxy. An http:// proxy is right for https:// targets: the proxy sees the hostname on the CONNECT line and nothing else.

Custom headers on the CONNECT request

Transport.ProxyConnectHeader adds headers to the CONNECT request only, and GetProxyConnectHeader computes them per proxy. You do not need either for authentication (userinfo covers it), but they are where a proxy-specific header goes if one is ever required.

ProxyFromEnvironment: HTTP_PROXY and HTTPS_PROXY

http.DefaultClient and any Transport with Proxy: http.ProxyFromEnvironment read the environment:

export HTTPS_PROXY="http://$PROXY_USER:$PROXY_PASS@$PROXY_HOST:$PROXY_PORT"
go run .

The variable is chosen by the target's scheme. In our run, with only HTTP_PROXY set, a request to an https:// URL went straight out with no proxy at all. Set HTTPS_PROXY for HTTPS sites, NO_PROXY for exclusions. Go caches the values the first time they are read, so changing them mid-process does nothing, and requests to localhost never use them.

Golang SOCKS5 proxy: two ways

Stdlib: a socks5:// URL in Transport.Proxy

Transport.Proxy accepts socks5:// and socks5h://, with credentials in the userinfo. Point it at the SOCKS5 port from the order:

socksURL := &url.URL{
	Scheme: "socks5",
	User:   url.UserPassword(os.Getenv("PROXY_USER"), os.Getenv("PROXY_PASS")),
	Host:   net.JoinHostPort(os.Getenv("PROXY_HOST"), os.Getenv("SOCKS5_PORT")),
}
client := &http.Client{
	Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
	Timeout:   30 * time.Second,
}

Unlike cURL and Python, Go treats socks5 the same as socks5h: the net/http documentation says so, and our SOCKS server logged a domain name, not an IP, for both. Hostnames always resolve on the proxy.

golang.org/x/net/proxy for raw connections

If you need SOCKS5 for something other than HTTP, or want the dialer itself, use x/net/proxy. Its latest release, v0.59.0, requires Go 1.26.

auth := &proxy.Auth{User: os.Getenv("PROXY_USER"), Password: os.Getenv("PROXY_PASS")}
dialer, err := proxy.SOCKS5("tcp", net.JoinHostPort(os.Getenv("PROXY_HOST"), os.Getenv("SOCKS5_PORT")), auth, proxy.Direct)
if err != nil {
	log.Fatal(err)
}
client := &http.Client{
	Transport: &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext},
	Timeout:   30 * time.Second,
}

For plain HTTP traffic, the stdlib URL form is shorter and does the same job. HTTP vs SOCKS5 proxies covers when SOCKS5 is worth choosing.

Go proxy rotation: one Transport per endpoint

The obvious approach is a Proxy func that returns the next URL on each call. We tested it against two proxies and four requests to an HTTP/2 site: every request went through the first proxy. Go pools HTTP/2 connections by target host, so once one exists, it is reused whatever your func returns. It only rotated after we forced HTTP/1.1 or disabled keep-alives.

The pattern that holds is one client per endpoint, chosen per request:

type pool struct {
	clients []*http.Client
	next    atomic.Uint64
}

func newPool(proxyURLs []string) (*pool, error) {
	p := &pool{}
	for _, raw := range proxyURLs {
		u, err := url.Parse(raw)
		if err != nil {
			return nil, err
		}
		p.clients = append(p.clients, &http.Client{
			Transport: &http.Transport{
				Proxy:                 http.ProxyURL(u),
				DialContext:           (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
				TLSHandshakeTimeout:   10 * time.Second,
				ResponseHeaderTimeout: 20 * time.Second,
				ForceAttemptHTTP2:     true,
			},
			Timeout: 30 * time.Second,
		})
	}
	return p, nil
}

func (p *pool) Client() *http.Client {
	return p.clients[(p.next.Add(1)-1)%uint64(len(p.clients))]
}

Load it from a comma-separated PROXY_URLS of http://USER:PASS@HOST:PORT entries and call p.Client().Get(...). Each endpoint keeps its own warm connections, and the SOCKS log confirmed requests alternating between both proxies. Static ISP and datacenter IPs suit this: each address stays yours for the term, so rotation is a choice you make, not something the network does to you.

Timeouts

A Transport without timeouts can wait on a stalled proxy for a long time. The pool above shows the set we use: Client.Timeout caps the whole request including the body, net.Dialer.Timeout caps the TCP connect to the proxy, TLSHandshakeTimeout covers the handshake inside the tunnel, and ResponseHeaderTimeout covers a target that accepts and then says nothing. Setting a custom DialContext turns off automatic HTTP/2, which is why ForceAttemptHTTP2 is there.

Common errors

ErrorCauseFix
Proxy Authentication Required (HTTPS target)Wrong credentials on the CONNECTRe-copy from the order; build the URL with url.UserPassword
Status 407 with no error (HTTP target)Same, returned as a responseSame fix; check resp.StatusCode
socks connect ... username/password authentication failedWrong SOCKS credentialsRe-copy them
invalid port ":PORT" after hostPlaceholder left in a string URLFill in the real values
Request goes directOnly HTTP_PROXY set for an HTTPS URLSet HTTPS_PROXY

A 403 or 429 after a successful tunnel comes from the target site, not the proxy: see proxy error codes explained.

Next steps

  • Test the same endpoint from the shell with cURL before debugging Go.
  • Compare the line prices on ISP pricing.
  • Connection reference and copy formats: the docs.