Go (Golang)
Runnable examples for net/http Transport configuration, SOCKS5 dialling, connection pooling and rotation control.
Go's standard library has thorough proxy support. Two things matter most: Transport is a heavyweight object meant to be reused, and DisableKeepAlives is the switch that controls IP rotation.
Basic usage
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
proxyURL, err := url.Parse(
"http://USERNAME-country-US-session-job1-time-30:PASSWORD@GATEWAY_HOST:58971",
)
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 30 * time.Second,
}
resp, err := client.Get("https://api.ipify.org?format=json")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode, string(body))
}
Reusing the Transport
Transport maintains the connection pool internally and must be reused. Creating one per request leaks connections and repeats handshakes needlessly.
package proxypool
import (
"net"
"net/http"
"net/url"
"time"
)
// NewClient builds a client intended for long-term reuse.
func NewClient(username, password, gateway string) (*http.Client, error) {
proxyURL := &url.URL{
Scheme: "http",
User: url.UserPassword(username, password),
Host: gateway,
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialContext: (&net.Dialer{
// The gateway's exit connect timeout is 8s; give the client a bit more
Timeout: 15 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 16,
// The gateway idles out at 5 minutes; going shorter avoids reusing a closed connection
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 15 * time.Second,
}
return &http.Client{Transport: transport, Timeout: 60 * time.Second}, nil
}
Rotating IPs
Omitting session is only the first step; connections must also not be reused. That's what DisableKeepAlives is for:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
func main() {
proxyURL := &url.URL{
Scheme: "http",
User: url.UserPassword("USERNAME-country-US", "PASSWORD"),
Host: "GATEWAY_HOST:58971",
}
// DisableKeepAlives forces a new connection per request, so routing runs again
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DisableKeepAlives: true,
},
Timeout: 30 * time.Second,
}
for i := 0; i < 5; i++ {
resp, err := client.Get("https://api.ipify.org")
if err != nil {
fmt.Println("failed:", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(body))
}
}
Concurrency with per-task sessions
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
)
const (
gateway = "GATEWAY_HOST:58971"
baseUser = "USERNAME"
password = "PASSWORD"
)
// clientFor builds a dedicated client for one session.
// session values allow only [A-Za-z0-9.-], hence dots rather than underscores.
func clientFor(sessionID, country string, minutes int) *http.Client {
user := fmt.Sprintf("%s-country-%s-session-%s-time-%d",
baseUser, country, sessionID, minutes)
proxyURL := &url.URL{
Scheme: "http",
User: url.UserPassword(user, password),
Host: gateway,
}
return &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 30 * time.Second,
}
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
client := clientFor(fmt.Sprintf("task.%d", id), "US", 30)
defer client.CloseIdleConnections()
resp, err := client.Get("https://api.ipify.org")
if err != nil {
fmt.Printf("task %d failed: %v\n", id, err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("task %d -> %s\n", id, body)
}(i)
}
wg.Wait()
}
SOCKS5
The standard library has no SOCKS5 client; use golang.org/x/net/proxy:
go get golang.org/x/net/proxy
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"time"
"golang.org/x/net/proxy"
)
func main() {
auth := &proxy.Auth{
User: "USERNAME-country-US-session-job1-time-30",
Password: "PASSWORD",
}
dialer, err := proxy.SOCKS5("tcp", "GATEWAY_HOST:58971", auth, proxy.Direct)
if err != nil {
panic(err)
}
// DialContext is what hands the hostname to the proxy (equivalent to socks5h)
contextDialer, ok := dialer.(proxy.ContextDialer)
if !ok {
panic("dialer does not support context")
}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return contextDialer.DialContext(ctx, network, addr)
},
},
Timeout: 30 * time.Second,
}
resp, err := client.Get("https://api.ipify.org")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
A wrapper with retries
Use bounded backoff for 502, 503 and 504; fix requests, quota or permissions before retrying 400, 402, 403 or 407. CONNECT handshake failures may appear as an error rather than an http.Response:
package proxypool
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
)
var ErrPermanent = errors.New("request deterministically refused by the gateway")
// GetWithRetry retries transient failures only.
func GetWithRetry(ctx context.Context, client *http.Client, url string, attempts int) ([]byte, error) {
var lastErr error
for attempt := 0; attempt < attempts; attempt++ {
if attempt > 0 {
delay := time.Duration(1<<attempt) * time.Second
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
lastErr = err
continue
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
switch {
// 400 / 402 / 403 / 407: fix options, quota or permissions first
case resp.StatusCode == http.StatusBadRequest,
resp.StatusCode == http.StatusPaymentRequired,
resp.StatusCode == http.StatusForbidden,
resp.StatusCode == http.StatusProxyAuthRequired:
return nil, fmt.Errorf("%w: HTTP %d", ErrPermanent, resp.StatusCode)
// 502 / 503 / 504: temporary connection failure, unavailable service or timeout
case resp.StatusCode >= 500:
lastErr = fmt.Errorf("exit failure: HTTP %d", resp.StatusCode)
continue
case readErr != nil:
lastErr = readErr
continue
default:
return body, nil
}
}
return nil, fmt.Errorf("still failing after %d attempts: %w", attempts, lastErr)
}
Building a pool from the Extract API
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type extractItem struct {
IP string `json:"ip"`
Port int `json:"port"`
}
type extractResponse struct {
Code int `json:"code"`
Data []extractItem `json:"data"`
}
func fetchProxyPool(apiKey string, count int) ([]string, error) {
query := url.Values{}
query.Set("apikey", apiKey)
query.Set("num", fmt.Sprint(count))
query.Set("type", "json")
query.Set("country", "US")
query.Set("session", "sticky")
query.Set("time", "10")
endpoint := "https://api.example.com/api/v1/proxy/extract?" + query.Encode()
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("extraction failed: HTTP %d", resp.StatusCode)
}
var payload extractResponse
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
// Passwordless ports, no credentials needed
pool := make([]string, 0, len(payload.Data))
for _, item := range payload.Data {
pool = append(pool, fmt.Sprintf("http://%s:%d", item.IP, item.Port))
}
return pool, nil
}
Troubleshooting checklist
| Symptom | Likely cause |
|---|---|
Proxy Authentication Required | Wrong credentials; password not passed through url.UserPassword, so escaping broke |
400 | invalid_proxy_parameters: option syntax, order or dependency |
402 | traffic_exhausted / account_quota_exhausted / quota_exhausted: traffic or quota exhausted |
403 | account_disabled / account_expired / access_denied / target_denied: account or policy denial |
429 | connection_limit: reduce concurrency and connection rate |
502 | Exit connection failed |
503 | No eligible route or service temporarily unavailable |
504 | Proxy connection establishment timed out |
| Connection count exploding | A new Transport per request instead of reuse |
| IP never changes | DisableKeepAlives not set, so connections are reused |
| SOCKS5 leaks local DNS | Used Dial instead of DialContext |
For HTTP / CONNECT failures, inspect X-NextProxy-Error and JSON error.code. Handle destination statuses and CONNECT handshake exceptions separately. See the error reference for the full mapping.