Skip to content
NextProxyNextProxyDocs

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

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

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

go
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

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

shell
go get golang.org/x/net/proxy
go
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:

go
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

go
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

SymptomLikely cause
Proxy Authentication RequiredWrong credentials; password not passed through url.UserPassword, so escaping broke
400invalid_proxy_parameters: option syntax, order or dependency
402traffic_exhausted / account_quota_exhausted / quota_exhausted: traffic or quota exhausted
403account_disabled / account_expired / access_denied / target_denied: account or policy denial
429connection_limit: reduce concurrency and connection rate
502Exit connection failed
503No eligible route or service temporarily unavailable
504Proxy connection establishment timed out
Connection count explodingA new Transport per request instead of reuse
IP never changesDisableKeepAlives not set, so connections are reused
SOCKS5 leaks local DNSUsed 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.

Did this page solve your problem?