سرویس Go شما باید درخواستهایش را از طریق پروکسی بفرستد: یک نقطه خروج در کشوری دیگر، آدرس ثابتی که شریک تجاریتان در لیست سفید گذاشته است، یا استخری از آدرسها تا یک آدرس همه درخواستها را حمل نکند. Go برای این کار به بسته اضافهای نیاز ندارد؛ پشتیبانی از پروکسی، از جمله SOCKS5 و متغیرهای محیطی استاندارد، در خود net/http ساخته شده است. غافلگیریها در پیشفرضها پنهاناند: http.Get همین حالا هم HTTPS_PROXY را میخواند، localhost هرگز از پروکسی نمیگذرد، متغیرها در هر فرایند فقط یک بار خوانده میشوند و یک اتصال HTTP/2 میتواند تابع پروکسیای را که برای چرخش نوشتهاید بیصدا نادیده بگیرد.
در این راهنما http.ProxyURL، ProxyFromEnvironment و قواعد NO_PROXY، هدر Proxy-Authorization، هر دو شکل SOCKS5 در Go، پروکسی متفاوت برای هر درخواست، مهلتها (timeout)، تلاش دوباره، متن دقیق خطاها و یک برنامه دریافت کامل را مرور میکنیم. همه نمونهها روی Go 1.26.6 (با golang.org/x/net v0.62.0) در برابر یک پروکسی HTTP محلی که user:pass میخواست و یک سرور SOCKS5 محلی اجرا شدند؛ خروجیها و سطرهای لاگ واقعیاند و فقط میزبان پروکسی با pr.proxynet.io:8000 جایگزین شده است. سمت Node.js همین کار را در استفاده از پروکسی در Node.js آوردهایم.
net/http چگونه تصمیم میگیرد از کدام پروکسی استفاده کند؟
هر درخواست از یک http.Transport میگذرد که فیلد Proxy آن تابعی است که درخواست را میگیرد و آدرس پروکسی، یا برای اتصال مستقیم nil، را برمیگرداند. طرح (scheme) آدرس نوع پروکسی را تعیین میکند: مستندات Transport در net/http طرحهای http، https، socks5 و socks5h را فهرست میکند، طرح خالی به معنای http است و اطلاعات ورود درون آدرس به هدر Proxy-Authorization تبدیل میشود. http.DefaultTransport که پشت http.Get قرار دارد، Proxy را روی http.ProxyFromEnvironment میگذارد؛ Transport که خودتان میسازید تا وقتی چیزی تنظیم نکنید از هیچ پروکسیای استفاده نمیکند.
سپس، بسته به مقصد:
- Transport به پروکسی وصل میشود، نه به مقصد؛ اگر پورت نوشته نشده باشد، برای
httpپورت 80، برایhttpsپورت 443 و برای SOCKS پورت 1080 به کار میرود. - مقصد ساده
http://. درخواست با URL کامل در سطر درخواست به پروکسی میرود وProxy-Authorizationهمراه هر درخواست فرستاده میشود. - مقصد
https://. Go یک بارCONNECT host:443را همراه این هدر میفرستد؛ پس از پاسخ200پروکسی، دستدادن TLS درون تونل انجام میشود و پروکسی فقط میزبان و پورت را میبیند. متد CONNECT در RFC 9110 تعریف شده است. - پروکسی
socks5://. Go دستدادن SOCKS را انجام میدهد، اگر آدرس اطلاعات ورود داشته باشد وارد میشود و نام میزبان مقصد را میفرستد، پس DNS را خود پروکسی resolve میکند. - اتصال در مخزن نگه داشته میشود تا درخواست بعدی به همان مقصد از طریق همان پروکسی، روی keep-alive در HTTP/1.1 یا روی HTTP/2، از آن استفاده کند؛ اتصالی که دوباره استفاده میشود
Proxyرا دوباره فراخوانی نمیکند.
چگونه با http.ProxyURL پروکسی تنظیم کنیم؟
http.ProxyURL یک آدرس ثابت را در تابعی با شکل درست میپیچد. آدرس را یک بار تجزیه کنید، در یک Transport بگذارید و Transport را به Client بدهید که مهلت هم دارد:
package main
import (
"cmp"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
func main() {
// The endpoint builder in the panel prints this string. Keep it in an environment variable, not in git.
raw := cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000")
proxyURL, err := url.Parse(raw)
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 20 * time.Second,
}
for _, target := range []string{"https://httpbin.org/ip", "http://httpbin.org/ip"} {
resp, err := client.Get(target)
if err != nil {
fmt.Println("error:", err)
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%s -> %d %s", target, resp.StatusCode, body)
}
}https://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}
http://httpbin.org/ip -> 200 {
"origin": "203.0.113.42"
}در سمت پروکسی این دو با هم فرق دارند: یک CONNECT برای مقصد HTTPS و یک GET با URL کامل که برای مقصد HTTP جلو فرستاده شده است، هر دو همراه اطلاعات ورود:
13:25:51.052 [http-proxy] CONNECT httpbin.org:443 proxy-authorization=Basic user -> 200 (tunnel #1)
13:25:51.563 [http-proxy] GET http://httpbin.org/ip proxy-authorization=Basic user user-agent=Go-http-client/1.1 -> forwardدو عادت: پس از هر پاسخ resp.Body.Close() را فراخوانی کنید، وگرنه اتصال نمیتواند به مخزن برگردد، و روی Client یک Timeout بگذارید، وگرنه یک نقطه خروج که پاسخ دادن را متوقف کند، goroutine را برای همیشه نگه میدارد. User-Agent پیشفرض Go-http-client/1.1 است؛ مقدار خودتان را تنظیم کنید تا مقصد بتواند برنامه شما را بشناسد.
Go متغیرهای HTTP_PROXY، HTTPS_PROXY و NO_PROXY را چگونه میخواند؟
http.ProxyFromEnvironment این سه متغیر، یا شکل با حروف کوچک آنها، را برای هر درخواست میخواند؛ قواعد در مستندات بسته httpproxy آمده است، همان پیادهسازیای که net/http به کار میبرد:
HTTP_PROXYبرای مقصدهایhttp://وHTTPS_PROXYبرای مقصدهایhttps://به کار میرود: طرح مقصد تعیینکننده است، نه طرح پروکسی، پس آدرس پروکسیhttp://درHTTPS_PROXYعادی است.- مقدار یا یک URL کامل است یا
host[:port]خالی کهhttpفرض میشود؛ هر چیز دیگری درخواست را ناموفق میکند. - اگر هم متغیر با حروف بزرگ و هم با حروف کوچک وجود داشته باشد، حروف کوچک برنده است.
NO_PROXYآدرسهای آیپی، بازههای CIDR مانند10.0.0.0/8و نامهای دامنه را فهرست میکند که هر کدام میتواند پورت هم داشته باشد. یک دامنه با خودش و زیردامنههایش مطابقت دارد؛ نامی که با نقطه شروع شود فقط با زیردامنهها مطابقت دارد؛ یک*تنها پروکسی را به کلی غیرفعال میکند.localhostو آدرسهای loopback هرگز از پروکسی نمیگذرند.
این برنامه بدون فرستادن چیزی، درباره پنج مقصد از ProxyFromEnvironment میپرسد:
package main
import (
"fmt"
"net/http"
)
func main() {
targets := []string{
"https://httpbin.org/ip",
"http://httpbin.org/ip",
"https://api.internal.example.com/v1",
"http://10.0.0.7:8080/health",
"http://localhost:9000/",
}
for _, target := range targets {
req, _ := http.NewRequest(http.MethodGet, target, nil)
proxyURL, err := http.ProxyFromEnvironment(req)
switch {
case err != nil:
fmt.Printf("%-36s error: %v\n", target, err)
case proxyURL == nil:
fmt.Printf("%-36s direct\n", target)
default:
fmt.Printf("%-36s via %s\n", target, proxyURL.Redacted())
}
}
}وقتی HTTPS_PROXY و HTTP_PROXY هر دو تنظیم باشند و NO_PROXY=.example.com,10.0.0.0/8:
https://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health direct
http://localhost:9000/ directوقتی فقط HTTP_PROXY تنظیم باشد، مقصدهای HTTPS مستقیم میروند؛ رایجترین دلیل اینکه یک برنامه Go «پروکسی را نادیده میگیرد»:
https://httpbin.org/ip direct
http://httpbin.org/ip via http://user:xxxxx@pr.proxynet.io:8000
https://api.internal.example.com/v1 direct
http://10.0.0.7:8080/health via http://user:xxxxx@pr.proxynet.io:8000
http://localhost:9000/ directRedacted() رمز را به شکل xxxxx میپوشاند؛ در لاگها از آن استفاده کنید.
یک قاعده در معنای متغیرها نیامده است: net/http محیط را یک بار، در نخستین درخواست، میخواند و برای تمام عمر فرایند نگه میدارد، پس os.Setenv پس از آن اثری ندارد:
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000برای اینکه محیط تغییریافته در زمان اجرا خوانده شود، httpproxy.FromEnvironment().ProxyFunc() را، که متغیرها را در هر فراخوانی میخواند، در Transport.Proxy بگذارید.
Go نام کاربری و رمز پروکسی را چگونه میفرستد؟
وقتی آدرس پروکسی نام کاربری و رمز داشته باشد، Go خودش Proxy-Authorization: Basic … را میفرستد. برای مقصد ساده http:// این هدر همراه هر درخواست میرود، حتی روی اتصال keep-alive: در آزمون تلاش دوباره ما، چهار GET روی یک سوکت هر کدام آن را حمل کردند. برای مقصد https:// یک بار، در CONNECT، فرستاده میشود و تونل تا وقتی اتصال زنده است دوباره استفاده میشود.
نویسههایی مانند @، : یا / در رمز، اگر با کدگذاری درصدی (percent-encoding) نوشته نشوند، آدرس را خراب میکنند؛ پس آدرس را از اجزایش بسازید:
proxyURL := &url.URL{
Scheme: "http",
Host: "pr.proxynet.io:8000",
User: url.UserPassword("user", "p@ss:word"), // encoded as p%40ss%3Aword on the wire
}Basic تنها روشی است که Go به پروکسی میفرستد؛ NTLM یا Kerberos به بسته شخص ثالث نیاز دارد. گیتوی یک ارائهدهنده Basic یا لیست سفید آیپی را میپذیرد؛ لیست سفید رمزی در آدرس باقی نمیگذارد و برای سروری با آدرس ثابت مناسب است. این دو را در احراز هویت پروکسی: نام کاربری و رمز یا لیست سفید آیپی مقایسه کردهایم.
چگونه در Go از پروکسی SOCKS5 استفاده کنیم؟
دو راه وجود دارد. Transport.Proxy آدرس socks5:// را میپذیرد و دستدادن را خودش انجام میدهد؛ Go با socks5 دقیقاً مانند socks5h رفتار میکند، پس نام میزبان مقصد را خود پروکسی resolve میکند. بسته golang.org/x/net/proxy در عوض dialer مربوط به SOCKS5 را بهصورت یک مقدار به شما میدهد که آن را در Transport.DialContext میگذارید و برای اتصالهایی که HTTP نیستند هم میتوانید به کار ببرید.
package main
import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"time"
"golang.org/x/net/proxy"
)
func main() {
// SOCKS_URL=socks5://user:pass@pr.proxynet.io:<the SOCKS5 port the endpoint builder shows>
socksURL, err := url.Parse(os.Getenv("SOCKS_URL"))
if err != nil || socksURL.Host == "" {
log.Fatal("set SOCKS_URL to socks5://user:pass@host:port")
}
// 1. net/http handles socks5:// on its own. The proxy resolves the host name.
viaTransport := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
Timeout: 20 * time.Second,
}
show("Transport.Proxy", viaTransport)
// 2. golang.org/x/net/proxy gives you the SOCKS5 dialer itself, usable for any TCP connection.
password, _ := socksURL.User.Password()
auth := &proxy.Auth{User: socksURL.User.Username(), Password: password}
dialer, err := proxy.SOCKS5("tcp", socksURL.Host, auth, &net.Dialer{Timeout: 10 * time.Second})
if err != nil {
log.Fatal(err)
}
viaDialer := &http.Client{
Transport: &http.Transport{DialContext: dialer.(proxy.ContextDialer).DialContext},
Timeout: 20 * time.Second,
}
show("x/net/proxy dialer", viaDialer)
}
func show(label string, client *http.Client) {
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
fmt.Printf("%-19s error: %v\n", label, err)
return
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%-19s %d %s", label, resp.StatusCode, body)
}Transport.Proxy 200 {
"origin": "203.0.113.42"
}
x/net/proxy dialer 200 {
"origin": "203.0.113.42"
}سرور SOCKS5 ما برای هر دو کلاینت همان سه مرحله را ثبت کرد: Go روشهای «بدون احراز هویت» و «نام کاربری/رمز» را پیشنهاد داد، سرور نام کاربری/رمز را آنطور که در RFC 1929 تعریف شده انتخاب کرد و درخواست اتصال نام دامنه را حمل میکرد، نه آدرس آیپی:
13:26:07.490 [socks5] greeting methods=[0,2]
13:26:07.490 [socks5] auth user=user -> OK
13:26:07.490 [socks5] CONNECT domain httpbin.org:443در مسیر dialer، Transport.Proxy را خالی بگذارید؛ dialer خودش از پروکسی میگذرد. SOCKS5 هم TCP و هم UDP را حمل میکند، اما net/http فقط اتصال TCP باز میکند. پروکسی SOCKS5 در Proxynet روی پورت جداگانهای کنار پورت HTTP کار میکند، با همان نام کاربری و رمز.
کدام روش برای کدام وضعیت مناسب است؟
| روش | کجا قرار میگیرد | دامنه اثر | نکته |
|---|---|---|---|
http.ProxyURL(u) | Transport.Proxy | همان Transport | یک پروکسی ثابت |
http.ProxyFromEnvironment | Transport.Proxy (پیشفرض در DefaultTransport) | کل فرایند | یک بار خوانده میشود؛ localhost مستثناست |
تابع func(*http.Request) خودتان | Transport.Proxy | هر درخواست | چرخش، قواعد به تفکیک میزبان |
آدرس socks5:// | هر یک از سه مورد بالا | همان Transport | DNS در سمت پروکسی resolve میشود |
dialer از x/net/proxy | Transport.DialContext | همان Transport | TCP خام هم؛ Proxy را خالی بگذارید |
چگونه برای هر درخواست از پروکسی متفاوتی استفاده کنیم؟
Transport.Proxy یک تابع معمولی است، پس میتواند در هر فراخوانی آدرس متفاوتی برگرداند. این تابع از یک فهرست به نوبت (round-robin) انتخاب میکند و انتخابش را چاپ میکند:
var next atomic.Uint64
transport := &http.Transport{
// Round-robin over the list. Called when the Transport needs a connection for a request.
Proxy: func(req *http.Request) (*url.URL, error) {
u := proxies[(next.Add(1)-1)%uint64(len(proxies))]
fmt.Printf("Proxy func: %s %s via %s\n", req.Method, req.URL, u.Redacted())
return u, nil
},
DisableKeepAlives: true, // a new connection, and therefore a new proxy choice, for every request
}سطر آخر مهم است. چهار GET به https://httpbin.org/ip فرستادیم، که HTTP/2 را مذاکره میکند، با دو پروکسی در فهرست، و فراخوانیها و تونلها را شمردیم:
| تنظیمات Transport | فراخوانی تابع Proxy | تونلهای بازشده |
|---|---|---|
| پیشفرضها (HTTP/2 مذاکره شد) | 1 | 1، فقط پروکسی اول |
فقط HTTP/1.1 (با map خالی برای TLSNextProto) | 4 | 2، یکی بهازای هر پروکسی، با استفاده دوباره |
DisableKeepAlives: true | 4 | 4، به تناوب |
با پیشفرضها، Go یک اتصال HTTP/2 از طریق پروکسی اول باز کرد و هر چهار درخواست را روی همان اتصال فرستاد؛ دیگر هرگز از تابع پرسیده نشد. با keep-alive در HTTP/1.1 هر بار پرسیده شد، اما درخواستهای سوم و چهارم اتصال بیکار به پروکسی خود را دوباره به کار بردند. فقط DisableKeepAlives، یا req.Close = true، بهازای هر درخواست یک تونل میدهد. گیتوی چرخشی هم از همین قاعده پیروی میکند، چون آیپی خروجی هنگام باز شدن تونل انتخاب میشود: گیتوی پروکسی مسکونی برای هر تونل تازه از طریق یک آدرس، خروجی را میچرخاند و یک پارامتر نشست در نام کاربری، برای جریانهای ورود، یک خروجی را 1 تا 60 دقیقه نگه میدارد.
مهلتها، محدودیت اتصال و TLS
http.Client.Timeout تنها تنظیمی است که هرگز نباید صفر بماند: کل درخواست را، از اتصال تا آخرین بایت، پوشش میدهد. تنظیمهای ریزتر روی Transport هستند: DialContext با net.Dialer{Timeout: …} اتصال به پروکسی را محدود میکند، TLSHandshakeTimeout دستدادن درون تونل را و ResponseHeaderTimeout انتظار برای هدرها را؛ http.NewRequestWithContext با یک سررسید (deadline) همین کار را برای یک درخواست انجام میدهد.
سه فیلد تعیین میکنند چند اتصال باز میکنید. MaxConnsPerHost اتصالها بهازای هر میزبان را سقف میزند و اتصالهای بعدی را منتظر میگذارد؛ یک محدودکننده نرخ ارزان به تفکیک مقصد. پیشفرض MaxIdleConnsPerHost برابر 2 است؛ با workerهای بیشتر، اتصالهای تمامشده بهجای ماندن در مخزن بسته میشوند، پس آن را برابر تعداد workerها بگذارید. IdleConnTimeout اتصالهایی را که بیاستفاده ماندهاند میبندد.
برای TLS هرگز سراغ InsecureSkipVerify نروید. خطای x509: certificate signed by unknown authority پشت پروکسی یعنی یک پروکسی سازمانی TLS را با مرجع صدور گواهی (CA) خودش بازرسی میکند؛ آن CA را به tls.Config.RootCAs اضافه کنید. گیتوی یک ارائهدهنده هرگز به TLS درون تونل دست نمیزند، پس در آنجا خطا به گواهی مقصد اشاره دارد.
نمونه کامل: تلاش دوباره، backoff و سقف همروندی
برنامه دریافت زیر اجزا را کنار هم میگذارد: مهلتها و محدودیتهای مخزن بالا، تلاش دوباره برای خطاهای شبکه، 429 و 5xx با backoff نمایی و جزء تصادفی (jitter)، رعایت Retry-After، بدون تلاش دوباره روی 407، و حداکثر چهار درخواست همزمان:
// fetch.go - GET a list of URLs through a proxy with timeouts, retries, backoff and a concurrency cap.
package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"log"
"math/rand/v2"
"net"
"net/http"
"net/url"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
const (
maxAttempts = 4 // first try + 3 retries
maxInFlight = 4 // requests running at the same time
)
var (
retryStatus = map[int]bool{429: true, 500: true, 502: true, 503: true, 504: true}
errProxyAuth = errors.New("proxy rejected the credentials (407): check user:pass or the IP whitelist")
)
func newClient(proxyURL *url.URL) *http.Client {
return &http.Client{
Timeout: 30 * time.Second, // the whole request: connect, headers and body
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
MaxConnsPerHost: maxInFlight,
MaxIdleConnsPerHost: maxInFlight,
IdleConnTimeout: 60 * time.Second,
// A 407 on the CONNECT tunnel becomes a typed error instead of "Proxy Authentication Required".
OnProxyConnectResponse: func(_ context.Context, _ *url.URL, _ *http.Request, res *http.Response) error {
if res.StatusCode == http.StatusProxyAuthRequired {
return errProxyAuth
}
return nil
},
},
}
}
// backoff: 1, 2, 4 s ... capped at 30 s, plus up to 1 s of jitter so workers do not retry in lockstep.
func backoff(attempt int) time.Duration {
d := min(time.Duration(1<<(attempt-1))*time.Second, 30*time.Second)
return d + time.Duration(rand.IntN(1000))*time.Millisecond
}
func sleep(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
case <-time.After(d):
}
}
// fetch GETs one URL. Network errors, 429 and 5xx are retried; a 407 is not, because the
// credentials are wrong and another attempt cannot fix that.
func fetch(ctx context.Context, client *http.Client, target string) ([]byte, int, error) {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
if err != nil {
return nil, 0, err
}
req.Header.Set("User-Agent", "price-monitor/1.0 (+mailto:you@example.com)")
resp, err := client.Do(req)
if err != nil {
if errors.Is(err, errProxyAuth) || ctx.Err() != nil {
return nil, 0, err
}
lastErr = err
log.Printf("attempt %d/%d %s: %v", attempt, maxAttempts, target, err)
sleep(ctx, backoff(attempt))
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusProxyAuthRequired { // plain-http target: the 407 is a response
return body, resp.StatusCode, errProxyAuth
}
if !retryStatus[resp.StatusCode] {
return body, resp.StatusCode, err // 200, 404, 301 ... the caller decides
}
lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
if attempt == maxAttempts {
break
}
// The site asked us to slow down. Its own Retry-After wins over our schedule.
wait := backoff(attempt)
if secs, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(secs) * time.Second
}
log.Printf("attempt %d/%d %s: HTTP %d, sleeping %s", attempt, maxAttempts, target, resp.StatusCode, wait.Round(100*time.Millisecond))
sleep(ctx, wait)
}
return nil, 0, lastErr
}
func main() {
proxyURL, err := url.Parse(cmp.Or(os.Getenv("PROXY_URL"), "http://user:pass@pr.proxynet.io:8000"))
if err != nil {
log.Fatal(err)
}
targets := os.Args[1:]
if len(targets) == 0 {
for i := 1; i <= 4; i++ {
targets = append(targets, fmt.Sprintf("https://httpbin.org/ip?n=%d", i))
}
targets = append(targets, "https://httpbin.org/status/503")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := newClient(proxyURL)
sem := make(chan struct{}, maxInFlight)
var wg sync.WaitGroup
var ok, failed atomic.Int32
for _, target := range targets {
wg.Add(1)
sem <- struct{}{} // blocks while maxInFlight requests are running
go func() {
defer wg.Done()
defer func() { <-sem }()
started := time.Now()
body, status, err := fetch(ctx, client, target)
if err != nil {
failed.Add(1)
log.Printf("fail %s -> %v", target, err)
return
}
ok.Add(1)
log.Printf("ok %s -> %d, %d bytes in %s", target, status, len(body), time.Since(started).Round(100*time.Millisecond))
}()
}
wg.Wait()
log.Printf("done: %d ok, %d failed", ok.Load(), failed.Load())
}پنج URL، چهار worker و یک آدرس که همیشه 503 پاسخ میدهد تا مسیر تلاش دوباره آزموده شود:
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=1 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=2 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=4 -> 200, 32 bytes in 500ms
2026/10/28 16:30:56 attempt 1/4 https://httpbin.org/status/503: HTTP 503, sleeping 1.3s
2026/10/28 16:30:56 ok https://httpbin.org/ip?n=3 -> 200, 32 bytes in 700ms
2026/10/28 16:30:58 attempt 2/4 https://httpbin.org/status/503: HTTP 503, sleeping 2.8s
2026/10/28 16:31:00 attempt 3/4 https://httpbin.org/status/503: HTTP 503, sleeping 4.8s
2026/10/28 16:31:05 fail https://httpbin.org/status/503 -> HTTP 503
2026/10/28 16:31:05 done: 4 ok, 1 failedسه نکته طراحی. OnProxyConnectResponse وقتی اجرا میشود که پروکسی به CONNECT پاسخ میدهد، پیش از آنکه Go وضعیت غیر از 200 را به یک رشته خطای ساده تبدیل کند؛ خطایی با نوع مشخص (typed error) در آنجا به errors.Is اجازه میدهد حلقه تلاش دوباره را متوقف کند و با رمز نادرست، برنامه برای یک مقصد HTTPS و یک مقصد HTTP ساده fail … -> proxy rejected the credentials (407) را بدون حتی یک تلاش دوباره چاپ کرد. مجموعه تلاش دوباره عمداً کوچک است: 404 یا 403 همانطور که هست برگردانده میشود، چون نقطه خروج دیگری چیزی را که صفحه هست تغییر نمیدهد. در برابر سرور محلیای که Retry-After: 2 میفرستاد، حلقه بهجای زمانبندی خودش دقیقاً 2 ثانیه میان تلاشها خوابید.
چگونه بررسی کنیم که درخواست واقعاً از پروکسی میگذرد؟
از یک سرویس بازتاب (echo) بپرسید کدام آدرس را میبیند، یک بار با Proxy: nil و یک بار با پروکسی. این nil عمدی است: Transport بدون مقدار Proxy هرگز محیط را نمیخواند، پس سطر اول حتی با HTTPS_PROXY تنظیمشده هم آدرس خودتان است.
direct := &http.Client{Transport: &http.Transport{Proxy: nil}, Timeout: 20 * time.Second}
proxied := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, Timeout: 20 * time.Second}
fmt.Println("without proxy:", exitIP(direct)) // exitIP GETs https://api.ipify.org?format=json and returns the "ip" field
fmt.Println("with proxy: ", exitIP(proxied))با یک گیتوی واقعی، سطر دوم آدرس نقطه خروج را نشان میدهد. با پروکسی آزمون ما هر دو سطر یک آدرس چاپ کردند، چون پروکسی روی همان ماشین اجرا میشود؛ مدرک، سطر CONNECT api.ipify.org:443 در لاگ آن بود. دو سطر یکسان با یک پروکسی واقعی معمولاً یعنی HTTP_PROXY بدون HTTPS_PROXY، مقصدی در NO_PROXY، یا Transport ساختهشده بدون مقدار Proxy.
Colly چطور؟
Colly، پرکاربردترین فریمورک خزش در Go، روی http.Transport ساخته شده است، پس همه آنچه در بالا آمد درباره آن هم صدق میکند. بسته proxy آن یک RoundRobinProxySwitcher دارد که آدرسهای http://، https:// یا socks5:// را میگیرد و یک colly.ProxyFunc برمیگرداند، با همان امضای Transport.Proxy؛ آن را با c.SetProxyFunc(rp) وصل میکنید، همانطور که در نمونه proxy switcher در مستندات Colly آمده است. این switcher بهازای هر درخواست فراخوانی میشود، اما قاعده استفاده دوباره از اتصال در بخش چرخش هنوز تعیین میکند که آیا یک درخواست تونل تازهای باز میکند یا نه.
خطاهای رایج و معنای آنها
پیامها همانطور که Go 1.26.6 در آزمونهای ما چاپ کرد، با میزبان جانشین:
Get "https://httpbin.org/ip": Proxy Authentication Requiredبرای مقصد HTTPS، یا وضعیت407بهصورت یک پاسخ عادی برای مقصد HTTP ساده:user:passنادرست، نویسههای خاص کدگذارینشده، یا آدرسی که در لیست سفید نیست. علتها را در رفع خطای HTTP 407 Proxy Authentication Required گرد آوردهایم.proxyconnect tcp: dial tcp pr.proxynet.io:8000: …بهعلاوه متن رد اتصال سیستمعامل (connect: connection refusedدر لینوکس و macOS،connectex:و یک جمله بومیسازیشده در ویندوز): چیزی آنجا گوش نمیدهد.proxyconnectمیگوید شکست در راه رسیدن به پروکسی بوده است، نه به مقصد.socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: یک آدرسsocks5://به پورت HTTP اشاره کرده است؛ 72 مقدار بایت حرفHدرHTTP/1.1است.socks connect tcp …: username/password authentication failed: سرور SOCKS5 اطلاعات ورود را رد کرد.unexpected EOF: یک آدرسhttp://روی پورت SOCKS5، که اتصال را هنگامCONNECTبست؛ طرح ناشناختهای مانندsocks4://مانندhttpدر نظر گرفته میشود و به همین شکل تمام میشود.proxyconnect tcp: tls: first record does not look like a TLS handshake: طرح پروکسیhttps://روی یک پورت HTTP ساده؛ ازhttp://استفاده کنید مگر اینکه ارائهدهنده بگوید پورت با TLS کار میکند.- پروکسی اصلاً استفاده نمیشود:
HTTP_PROXYبدونHTTPS_PROXY، مقصدی کهlocalhostاست یا درNO_PROXYقرار دارد، متغیرهایی که پس از نخستین درخواست تنظیم شدهاند، یا Transport ساختهشده بدون مقدارProxy.
این تنظیمات کجا به کار میرود؟
- بررسی قیمت و موجودی از چند کشور: پارامتر کشور در نام کاربری پروکسی و چرخش بهازای هر درخواست برای صفحههای محصول مستقل از هم.
- کلاینتهای API پشت یک آدرس مجاز، جایی که شریک تجاری یک آدرس ثابت را در لیست سفید گذاشته و سرویس باید از همان خارج شود.
- پایش از بیرون شبکه خودتان، تا بررسی سلامت سایت را همانطور ببیند که بازدیدکنندهای در منطقهای دیگر میبیند.
- workerها در شبکه سازمانی که فقط از طریق پروکسی شرکت به اینترنت میرسند.
اشتباهات رایج
- تنظیم فقط
HTTP_PROXY. مقصدهای HTTPS مستقیم میروند؛HTTPS_PROXYرا هم تنظیم کنید. - تغییر محیط پس از نخستین درخواست. net/http آن را نگه میدارد؛ فرایند را دوباره آغاز کنید یا از
httpproxyاستفاده کنید. - انتظار چرخش با keep-alive روشن. اتصالی که دوباره استفاده میشود هرگز
Proxyرا دوباره فراخوانی نمیکند. - فراموش کردن
resp.Body.Close(). اتصالها هرگز به مخزن برنمیگردند. - نبود
Client.Timeout. یک نقطه خروج خاموش، goroutine را برای همیشه مسدود میکند. - چسباندن رمزی با
@یا:در رشته URL. آن را باurl.UserPasswordبسازید. - تنظیم همزمان
Transport.ProxyوDialContextمربوط به SOCKS5. یکی را انتخاب کنید. - تلاش دوباره روی
407. تلاش دوم همان اطلاعات ورود نادرست را میفرستد.
کدام روش را انتخاب کنیم؟
| وضعیت شما | پیشنهاد |
|---|---|
| یک پروکسی، بدون تغییر کد | HTTPS_PROXY و HTTP_PROXY، و NO_PROXY برای میزبانهای داخلی |
| یک پروکسی، تنظیم در کد | Transport{Proxy: http.ProxyURL(u)} همراه Client.Timeout |
| نقطه خروج تازه برای هر درخواست | تابع Proxy خودتان یا یک گیتوی چرخشی، با DisableKeepAlives: true |
| همان نقطه خروج برای جریان ورود | پارامتر نشست در نام کاربری، keep-alive روشن |
| SOCKS5 با DNS در سمت پروکسی | socks5://user:pass@host:port در Transport.Proxy |
| SOCKS5 برای اتصال غیر HTTP | dialer از golang.org/x/net/proxy |
| workerهای زیاد روی یک سایت | MaxConnsPerHost و MaxIdleConnsPerHost برابر تعداد workerها |
| پروکسی سازمانی با CA خودش | CA در tls.Config.RootCAs، هرگز InsecureSkipVerify |
پرسشهای متداول
آیا http.Get در Go بهطور خودکار از پروکسی استفاده میکند؟
بله، وقتی محیط چنین بگوید: http.Get از http.DefaultClient استفاده میکند که Transport آن Proxy را روی http.ProxyFromEnvironment میگذارد. Transport که خودتان میسازید بدون پروکسی شروع میکند.
آیا این همان GOPROXY است؟
خیر. GOPROXY به فرمان go میگوید ماژولها را از کجا دانلود کند؛ ربطی به درخواستهایی که برنامه شما در زمان اجرا میفرستد ندارد.
چگونه فقط برای یک درخواست پروکسی تنظیم کنم؟
به آن درخواست Client و Transport مخصوص خودش را بدهید، یا تابع Proxy بنویسید که برای درخواستهایی که باید مستقیم بروند nil برگرداند. ممکن است اتصالی در مخزن به همان میزبان از قبل وجود داشته باشد؛ req.Close = true اتصال تازه را اجباری میکند.
آیا Go از socks5h پشتیبانی میکند؟
بله، و socks5 هم همانطور رفتار میکند: Go همیشه نام میزبان را به پروکسی میفرستد و پروکسی آن را resolve میکند؛ net/http حالت SOCKS5 با resolve محلی ندارد.
چرا localhost هرگز از پروکسی نمیگذرد؟
ProxyFromEnvironment برای localhost و آدرسهای loopback، هر چه متغیرها بگویند، پروکسی برنمیگرداند؛ برای عبور دادن چنین درخواستهایی از پروکسی، بهجای آن از http.ProxyURL استفاده کنید.
چگونه ببینم یک درخواست از کدام پروکسی استفاده کرده است؟
تابع Proxy را در تابع دیگری بپیچید و u.Redacted() را لاگ کنید، یا Transport.OnProxyConnectResponse را تنظیم کنید که آدرس پروکسی و پاسخ CONNECT را دریافت میکند. با keep-alive روشن، انتخاب فقط وقتی لاگ میشود که اتصال تازهای باز شود.
خلاصه
در Go پروکسی تابعی روی http.Transport است: http.ProxyURL برای آدرس ثابت، http.ProxyFromEnvironment برای متغیرهای استاندارد، یا تابع خودتان برای انتخاب بهازای هر درخواست. user:pass را در آدرس بگذارید تا Go خودش Proxy-Authorization را بفرستد؛ برای SOCKS5 از socks5:// استفاده کنید؛ Client.Timeout و محدودیتهای مخزن را تنظیم کنید؛ 429 و 5xx را با backoff دوباره امتحان کنید اما 407 را هرگز؛ وقتی هر درخواست نقطه خروج تازهای میخواهد keep-alive را خاموش کنید. نقطههای اتصال مسکونی، ISP ثابت، دیتاسنتر و SOCKS5 که با همین تنظیمات کار میکنند در صفحه پروکسیهای Proxynet آمدهاند.




