Go (Golang)
net/http Transport 配置、SOCKS5 拨号、连接池与轮换控制的可运行示例。
Go 的标准库对代理支持很完整,主要注意两点:Transport 是需要复用的重对象,而 DisableKeepAlives 是控制 IP 轮换的开关。
基本用法
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 内部维护连接池,必须复用。每个请求新建一个会导致连接泄漏和大量重复握手。
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:
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))
}
}
并发 + 每任务独立会话
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:
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 才能把域名交给代理解析(等价于 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))
}
带重试的封装
502、503、504 可有限退避重试;400、402、403、407 应先修正请求、额度或权限。CONNECT 握手失败可能通过 error 返回,而不是 http.Response:
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)
}
从提取接口拿代理池
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 导致转义错 |
400 | invalid_proxy_parameters:参数格式、顺序或依赖错误 |
402 | traffic_exhausted / account_quota_exhausted / quota_exhausted:流量或额度不足 |
403 | account_disabled / account_expired / access_denied / target_denied:账号或策略拒绝 |
429 | connection_limit:降低并发和建连频率 |
502 | 出口连接失败 |
503 | 暂无可用线路或服务暂不可用 |
504 | 建立代理连接超时 |
| 连接数暴涨 | Transport 每次新建了,没有复用 |
| IP 一直不变 | 没开 DisableKeepAlives,连接被复用 |
| SOCKS5 泄露本地 DNS | 用了 Dial 而不是 DialContext |
HTTP / CONNECT 失败可读取 X-NextProxy-Error 与 JSON 的 error.code;目标网站返回的状态和 CONNECT 握手错误需分别处理。完整映射见 错误码对照。