跳到主要内容
NextProxyNextProxy文档

Go (Golang)

net/http Transport 配置、SOCKS5 拨号、连接池与轮换控制的可运行示例。

Go 的标准库对代理支持很完整,主要注意两点:Transport 是需要复用的重对象,而 DisableKeepAlives 是控制 IP 轮换的开关。

基本用法

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))
}

复用 Transport

Transport 内部维护连接池,必须复用。每个请求新建一个会导致连接泄漏和大量重复握手。

go
package proxypool

import (
    "net"
    "net/http"
    "net/url"
    "time"
)

// NewClient 构造一个可长期复用的客户端。
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{
            // 网关建立出口连接的超时是 8 秒,客户端给稍宽一点
            Timeout:   15 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 16,
        // 网关空闲超时是 5 分钟,这里设更短以免用到已关闭的连接
        IdleConnTimeout:     90 * time.Second,
        TLSHandshakeTimeout: 15 * time.Second,
    }

    return &http.Client{Transport: transport, Timeout: 60 * time.Second}, nil
}

轮换 IP

不带 session 参数只是第一步,还得让连接不被复用。用 DisableKeepAlives

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 让每个请求都建新连接,从而重新选路
    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("失败:", err)
            continue
        }
        body, _ := io.ReadAll(resp.Body)
        resp.Body.Close()
        fmt.Println(string(body))
    }
}

并发 + 每任务独立会话

go
package main

import (
    "fmt"
    "io"
    "net/http"
    "net/url"
    "sync"
    "time"
)

const (
    gateway  = "GATEWAY_HOST:58971"
    baseUser = "USERNAME"
    password = "PASSWORD"
)

// clientFor 为一条会话构造独立的客户端。
// session 值只允许 [A-Za-z0-9.-],所以用点号而不是下划线。
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 失败: %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

标准库没有内置 SOCKS5 客户端,用 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 才能把域名交给代理解析(等价于 socks5h)
    contextDialer, ok := dialer.(proxy.ContextDialer)
    if !ok {
        panic("dialer 不支持 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))
}

带重试的封装

502503504 可有限退避重试;400402403407 应先修正请求、额度或权限。CONNECT 握手失败可能通过 error 返回,而不是 http.Response

go
package proxypool

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "time"
)

var ErrPermanent = errors.New("请求被网关确定性拒绝")

// GetWithRetry 只重试暂时性失败。
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:先修复参数、额度或权限
        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:暂时连接失败、服务不可用或超时
        case resp.StatusCode >= 500:
            lastErr = fmt.Errorf("出口失败: HTTP %d", resp.StatusCode)
            continue

        case readErr != nil:
            lastErr = readErr
            continue

        default:
            return body, nil
        }
    }

    return nil, fmt.Errorf("重试 %d 次后仍失败: %w", attempts, lastErr)
}

从提取接口拿代理池

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("提取失败: HTTP %d", resp.StatusCode)
    }

    var payload extractResponse
    if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
        return nil, err
    }

    // 免密端口,不需要凭据
    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
}

排错清单

现象常见原因
Proxy Authentication Required凭据错;密码没走 url.UserPassword 导致转义错
400invalid_proxy_parameters:参数格式、顺序或依赖错误
402traffic_exhausted / account_quota_exhausted / quota_exhausted:流量或额度不足
403account_disabled / account_expired / access_denied / target_denied:账号或策略拒绝
429connection_limit:降低并发和建连频率
502出口连接失败
503暂无可用线路或服务暂不可用
504建立代理连接超时
连接数暴涨Transport 每次新建了,没有复用
IP 一直不变没开 DisableKeepAlives,连接被复用
SOCKS5 泄露本地 DNS用了 Dial 而不是 DialContext

HTTP / CONNECT 失败可读取 X-NextProxy-Error 与 JSON 的 error.code;目标网站返回的状态和 CONNECT 握手错误需分别处理。完整映射见 错误码对照

这篇解决你的问题了吗?