Go (Golang) ile Proxy Kullanımı: net/http Proxy Ayarları

Yayın tarihi:

13 dk okuma

Acar Diveroli
Yazar: Acar Diveroli
Ölçülü bir rota üzerinde üç durak: net/http istemcisi, tünel simgeli büyük mavi proxy durağı ve 200 OK dönen hedef

Go ile yazdığınız servis, isteklerini bir proxy üzerinden göndermek zorunda: başka bir ülkeden çıkış, iş ortağınızın whitelist'e aldığı sabit bir IP ya da tek adresin bütün istekleri taşımaması için bir havuz. Go bunun için ek paket istemez; net/http proxy desteğini SOCKS5 ve standart ortam değişkenleri dahil kendi içinde taşır. Sürprizler varsayılanlarda gizlidir: http.Get zaten HTTPS_PROXY değişkenini okur, localhost hiçbir zaman proxy'den geçmez, değişkenler süreç başına bir kez okunur ve bir HTTP/2 bağlantısı rotasyon için yazdığınız proxy fonksiyonunu sessizce atlayabilir.

Bu yazıda http.ProxyURL, ProxyFromEnvironment ve NO_PROXY kurallarını, Proxy-Authorization başlığını, Go'nun iki SOCKS5 yolunu, istek başına farklı proxy seçimini, zaman aşımlarını, yeniden denemeyi, hata metinlerinin birebir hâlini ve eksiksiz bir indirme programını anlatıyoruz. Her örnek Go 1.26.6 (golang.org/x/net v0.62.0) ile, user:pass isteyen yerel bir HTTP proxy ve yerel bir SOCKS5 sunucusu üzerinde çalıştırıldı; çıktılar ve kayıt satırları gerçektir, yalnız proxy adresi pr.proxynet.io:8000 yer tutucusuyla değiştirildi. Aynı işin Node.js tarafı Node.js'te Proxy Kullanımı yazısında.

net/http hangi proxy'yi kullanacağına nasıl karar verir?

Her istek bir http.Transport üzerinden geçer ve Transport'un Proxy alanı, isteği alıp proxy adresini (ya da doğrudan bağlantı için nil) döndüren bir fonksiyondur. Proxy türünü adresin şeması belirler: net/http Transport belgesine göre http, https, socks5 ve socks5h desteklenir, boş şema http sayılır ve adresteki kimlik bilgileri Proxy-Authorization başlığına dönüşür. http.Get'in arkasındaki http.DefaultTransport bu alana http.ProxyFromEnvironment koyar; kendi kurduğunuz Transport ise siz ayarlayana kadar proxy kullanmaz.

Sonrası hedefe göre değişir:

  1. Transport hedefe değil proxy'ye bağlanır; port yazılmamışsa http için 80, https için 443, SOCKS için 1080 kullanılır.
  2. Düz http:// hedef. İstek, istek satırında tam URL ile proxy'ye gider ve her isteğe Proxy-Authorization eklenir.
  3. https:// hedef. Go, Proxy-Authorization başlığıyla birlikte bir kez CONNECT host:443 gönderir; proxy 200 dediğinde TLS el sıkışması tünelin içinde yapılır, proxy yalnız host ve portu görür. CONNECT yöntemi RFC 9110'da tanımlıdır.
  4. socks5:// proxy. Go SOCKS el sıkışmasını yapar, adreste kimlik bilgisi varsa oturum açar ve hedefin alan adını gönderir; DNS'i proxy çözer.
  5. Bağlantı havuzda tutulur. Aynı hedefe aynı proxy üzerinden giden sonraki istek, HTTP/1.1 keep-alive ya da HTTP/2 ile aynı bağlantıyı kullanır ve yeniden kullanılan bağlantı Proxy fonksiyonunu bir daha çağırmaz.

http.ProxyURL ile proxy nasıl ayarlanır?

http.ProxyURL, sabit bir adresi doğru biçimdeki fonksiyona sarar. Adresi bir kez ayrıştırın, Transport'a koyun ve Transport'u zaman aşımı tanımlı bir Client'a verin:

go
package main

import (
	"cmp"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"time"
)

func main() {
	// Bu satırı paneldeki endpoint oluşturucu verir. Git'e değil, ortam değişkenine yazın.
	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)
	}
}
text
https://httpbin.org/ip -> 200 {
  "origin": "203.0.113.42"
}
http://httpbin.org/ip -> 200 {
  "origin": "203.0.113.42"
}

Proxy tarafında iki istek farklı görünür: HTTPS hedef için bir CONNECT, HTTP hedef için tam URL ile iletilen bir GET, ikisi de kimlik bilgisiyle:

text
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

İki alışkanlık edinin: her yanıttan sonra resp.Body.Close(), yoksa bağlantı havuza dönemez; Client'ta Timeout, yoksa yanıt vermeyi kesen bir çıkış goroutine'i sonsuza kadar bekletir. Varsayılan User-Agent değeri Go-http-client/1.1'dir; hedefin programınızı tanıyabilmesi için kendi değerinizi yazın.

Go, HTTP_PROXY, HTTPS_PROXY ve NO_PROXY değişkenlerini nasıl okur?

http.ProxyFromEnvironment üç değişkeni ya da küçük harfli hâllerini her istekte uygular; kurallar, net/http'nin de kullandığı httpproxy paketinin belgesinde yazılıdır:

  • HTTP_PROXY yalnız http:// hedeflere, HTTPS_PROXY yalnız https:// hedeflere uygulanır. Karar hedefin şemasına göre verilir, proxy'ninkine göre değil; HTTPS_PROXY içinde http:// ile başlayan bir proxy adresi normaldir.
  • Değer tam bir URL ya da http sayılan çıplak bir host[:port] olabilir; başka bir biçim isteği hatayla düşürür.
  • Büyük ve küçük harfli değişken birlikte tanımlıysa küçük harfli olan kazanır.
  • NO_PROXY, virgülle ayrılmış IP adresleri, 10.0.0.0/8 gibi CIDR aralıkları ve alan adlarını listeler; her birine port eklenebilir. Alan adı kendisiyle ve bütün alt alan adlarıyla eşleşir; nokta ile başlayan ad yalnız alt alan adlarıyla eşleşir; tek bir * proxy'yi tamamen kapatır.
  • localhost ve loopback adresleri hiçbir zaman proxy'den geçmez.

Aşağıdaki program hiçbir şey göndermeden ProxyFromEnvironment'a beş hedef için ne yapacağını sorar:

go
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 ve HTTP_PROXY birlikte tanımlı, NO_PROXY=.example.com,10.0.0.0/8 iken:

text
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

Yalnız HTTP_PROXY tanımlıyken HTTPS hedefler doğrudan gider; bir Go programının "proxy'yi görmezden gelmesinin" en sık nedeni budur:

text
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/               direct

Redacted() parolayı xxxxx ile maskeler; kayıtlarda bunu kullanın.

Değişken kurallarında yazmayan bir kural daha var: net/http ortamı ilk istekte bir kez okur ve sürecin ömrü boyunca önbellekte tutar; sonradan os.Setenv ile yapılan değişiklik işe yaramaz:

text
before Setenv: <nil>
after Setenv, net/http: <nil>
after Setenv, httpproxy: http://user:xxxxx@pr.proxynet.io:8000

Çalışma sırasında değişen ortamı görmek için, değişkenleri her çağrıda yeniden okuyan httpproxy.FromEnvironment().ProxyFunc() fonksiyonunu Transport.Proxy alanına koyun.

Go, proxy kullanıcı adı ve parolasını nasıl gönderir?

Proxy adresinde kullanıcı adı ve parola varsa Go Proxy-Authorization: Basic … başlığını kendisi gönderir. Düz http:// hedefte başlık her istekle gider, keep-alive bağlantıda bile: yeniden deneme testimizde aynı soket üzerinden giden dört GET'in her biri başlığı taşıdı. https:// hedefte ise yalnız CONNECT isteğinde bir kez gönderilir ve tünel, bağlantı yaşadığı sürece yeniden kullanılır.

Paroladaki @, : ya da / gibi karakterler yüzde kodlaması yapılmazsa adresi bozar; adresi parçalardan kurun:

go
proxyURL := &url.URL{
	Scheme: "http",
	Host:   "pr.proxynet.io:8000",
	User:   url.UserPassword("user", "p@ss:word"), // ağa p%40ss%3Aword olarak kodlanmış gider
}

Go'nun proxy'ye gönderdiği tek şema Basic'tir; NTLM ya da Kerberos için üçüncü taraf paket gerekir. Sağlayıcı gate'i Basic'i ya da IP whitelist'i kabul eder; whitelist'te adreste parola hiç bulunmaz, bu da sabit adresli sunucuya daha uygundur. İki yöntemi Proxy Kimlik Doğrulama: User:Pass ve IP Whitelist yazısında karşılaştırdık.

Go'da SOCKS5 proxy nasıl kullanılır?

İki yol var. Transport.Proxy bir socks5:// adresi kabul eder ve el sıkışmayı kendisi yapar; Go socks5'i tam olarak socks5h gibi ele alır, yani hedefin alan adını proxy çözer. golang.org/x/net/proxy paketi ise SOCKS5 dialer'ını size bir değer olarak verir; bunu Transport.DialContext alanına bağlarsınız ve HTTP olmayan bağlantılarda da kullanabilirsiniz.

go
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:<endpoint oluşturucunun gösterdiği SOCKS5 portu>
	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 socks5:// adresini kendisi işler. Alan adını proxy çözer.
	viaTransport := &http.Client{
		Transport: &http.Transport{Proxy: http.ProxyURL(socksURL)},
		Timeout:   20 * time.Second,
	}
	show("Transport.Proxy", viaTransport)

	// 2. golang.org/x/net/proxy SOCKS5 dialer'ını verir; her TCP bağlantısında kullanılabilir.
	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)
}
text
Transport.Proxy     200 {
  "origin": "203.0.113.42"
}
x/net/proxy dialer  200 {
  "origin": "203.0.113.42"
}

SOCKS5 sunucumuz iki istemci için de aynı üç adımı kaydetti: Go "kimlik doğrulama yok" ve "kullanıcı adı/parola" yöntemlerini önerdi, sunucu RFC 1929'da tanımlı kullanıcı adı/parola yöntemini seçti ve bağlantı isteği IP değil alan adı taşıdı:

text
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 yolunda Transport.Proxy alanını boş bırakın; dialer zaten proxy'den geçer. SOCKS5 TCP ve UDP taşır ama net/http yalnız TCP bağlantısı açar. Proxynet'in SOCKS5 Proxy ürünü HTTP portunun yanında kendi portunda, aynı kullanıcı adı ve parolayla çalışır.

Hangi yöntem hangi duruma uyar?

YöntemNereye yazılırKapsamNot
http.ProxyURL(u)Transport.Proxyo Transporttek sabit proxy
http.ProxyFromEnvironmentTransport.Proxy (DefaultTransport varsayılanı)süreçbir kez okunur; localhost atlanır
kendi func(*http.Request) fonksiyonunuzTransport.Proxyistek başınarotasyon, host'a göre kural
socks5:// adresiyukarıdaki üçünden birio TransportDNS proxy'de çözülür
x/net/proxy dialer'ıTransport.DialContexto Transportham TCP de; Proxy boş kalır

Her istekte farklı proxy nasıl kullanılır?

Transport.Proxy sıradan bir fonksiyondur, her çağrıda farklı bir adres döndürebilir. Aşağıdaki örnek listeden sırayla seçer ve seçimini yazdırır:

go
var next atomic.Uint64
transport := &http.Transport{
	// Liste üzerinde sırayla döner. Transport bir istek için bağlantı seçerken çağrılır.
	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, // her istekte yeni bağlantı, dolayısıyla yeni proxy seçimi
}

Önemli olan son satır. HTTP/2 konuşan https://httpbin.org/ip adresine, listede iki proxy varken dört GET gönderdik; fonksiyonun kaç kez çağrıldığını ve proxy'lerin kaç tünel açtığını saydık:

Transport ayarıProxy fonksiyonu çağrısıAçılan tünel
Varsayılan (HTTP/2 anlaşıldı)11, yalnız ilk proxy
Yalnız HTTP/1.1 (boş TLSNextProto haritası)42, proxy başına bir, yeniden kullanıldı
DisableKeepAlives: true44, sırayla

Varsayılan ayarlarla Go ilk proxy üzerinden tek bir HTTP/2 bağlantısı açtı ve dört isteği de onun içinden geçirdi; fonksiyona bir daha sorulmadı. HTTP/1.1 keep-alive ile her seferinde soruldu, ama üçüncü ve dördüncü istek kendi proxy'sine açık duran boş bağlantıyı yeniden kullandı. İstek başına bir tüneli yalnız DisableKeepAlives ya da req.Close = true verir. Rotating gate de aynı kurala uyar, çünkü çıkış IP'si tünel açılırken seçilir: Residential Proxy gate'i tek adres üzerinden her yeni tünelde çıkışı değiştirir, giriş yapılan akışlarda ise kullanıcı adındaki oturum parametresi aynı çıkışı 1-60 dakika tutar.

Zaman aşımı, bağlantı sınırları ve TLS

http.Client.Timeout sıfırda bırakılmaması gereken tek ayardır: bağlantıdan son bayta kadar isteğin tamamını kapsar. İnce ayarlar Transport'tadır: net.Dialer{Timeout: …} ile verilen DialContext proxy'ye bağlanmayı, TLSHandshakeTimeout tünel içindeki el sıkışmayı, ResponseHeaderTimeout başlıkların beklenmesini sınırlar; tek bir istek için http.NewRequestWithContext ile verilen süre sınırı aynı işi görür.

Kaç bağlantı açacağınızı üç alan belirler. MaxConnsPerHost host başına bağlantı sayısını sınırlar ve fazlasını bekletir; hedef başına ucuz bir hız sınırlayıcıdır. MaxIdleConnsPerHost varsayılan olarak 2'dir; daha çok işçiniz varsa biten bağlantılar havuza dönmek yerine kapanır, bu yüzden işçi sayısına eşitleyin. IdleConnTimeout boşta kalan bağlantıları kapatır.

TLS'te InsecureSkipVerify seçeneğine hiç uzanmayın. Proxy arkasında görülen x509: certificate signed by unknown authority hatası, kurumsal bir proxy'nin TLS'i kendi sertifika otoritesiyle incelediğini söyler; o CA'yı tls.Config.RootCAs alanına ekleyin. Sağlayıcı gate'i tünelin içindeki TLS'e hiç dokunmaz, orada hata hedefin sertifikasını işaret eder.

Eksiksiz örnek: yeniden deneme, bekleme ve eşzamanlılık sınırı

Aşağıdaki program parçaları birleştirir: yukarıdaki zaman aşımları ve havuz sınırları, ağ hatalarında, 429 ve 5xx yanıtlarında üstel bekleme ve rastgele paylı yeniden deneme, Retry-After başlığına uyma, 407'de yeniden denememe ve aynı anda en fazla dört istek:

go
// fetch.go - bir URL listesini proxy üzerinden zaman aşımı, yeniden deneme, bekleme ve eşzamanlılık sınırıyla indirir.
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 // ilk deneme + 3 yeniden deneme
	maxInFlight = 4 // aynı anda çalışan istek sayısı
)

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, // isteğin tamamı: bağlantı, başlıklar ve gövde
		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,
			// CONNECT tünelindeki 407, "Proxy Authentication Required" metni yerine tipli bir hataya dönüşür.
			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 sn ... en çok 30 sn, artı 1 sn'ye kadar rastgele pay; işçiler aynı anda yeniden denemesin.
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 tek bir URL'yi GET ile alır. Ağ hataları, 429 ve 5xx yeniden denenir; 407 denenmez,
// çünkü kimlik bilgisi yanlıştır ve ikinci deneme bunu düzeltemez.
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 { // düz http hedefte 407 bir yanıt olarak gelir
			return body, resp.StatusCode, errProxyAuth
		}
		if !retryStatus[resp.StatusCode] {
			return body, resp.StatusCode, err // 200, 404, 301 ... kararı çağıran verir
		}
		lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
		if attempt == maxAttempts {
			break
		}
		// Site yavaşlamamızı istedi. Kendi Retry-After değeri bizim takvimimizden önce gelir.
		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{}{} // maxInFlight istek çalışırken bekletir
		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())
}

Beş URL, dört işçi, yeniden deneme yolunu çalıştırmak için hep 503 dönen bir adres:

text
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

Üç tasarım kararı var. OnProxyConnectResponse, proxy CONNECT'e yanıt verdiğinde, Go 200 dışındaki durumu düz bir hata metnine çevirmeden önce çalışır; orada döndürülen tipli hata errors.Is ile yeniden deneme döngüsünü durdurur. Yanlış parolayla aynı program hem HTTPS hem düz HTTP hedef için tek bir yeniden deneme yapmadan fail … -> proxy rejected the credentials (407) yazdı. Yeniden deneme kümesi bilerek küçük tutuldu: 404 ya da 403 olduğu gibi döner, çünkü başka bir çıkış sayfanın ne olduğunu değiştirmez. Retry-After: 2 gönderen yerel bir sunucuya karşı döngü kendi takvimi yerine denemeler arasında tam 2 saniye bekledi.

İsteğin gerçekten proxy'den geçtiği nasıl doğrulanır?

Gördüğü adresi geri söyleyen bir servise sorun; bir kez Proxy: nil ile, bir kez proxy ile. nil bilerek yazılır: Proxy değeri olmayan Transport ortamı hiç okumaz, bu yüzden HTTPS_PROXY tanımlı olsa bile ilk satır kendi adresinizdir.

go
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, https://api.ipify.org?format=json adresini çekip "ip" alanını döndürür
fmt.Println("with proxy:   ", exitIP(proxied))

Gerçek bir gate ile ikinci satır çıkışın adresini gösterir. Test proxy'mizle iki satır aynı adresi yazdı, çünkü proxy aynı makinede çalışıyor; kanıt, proxy kaydındaki CONNECT api.ipify.org:443 satırıydı. Gerçek proxy ile iki satır aynıysa genellikle neden HTTPS_PROXY olmadan HTTP_PROXY, NO_PROXY içindeki bir hedef ya da Proxy değeri olmadan kurulmuş bir Transport'tur.

Colly'de durum nedir?

Go'nun en yaygın web tarama çatısı Colly, http.Transport üzerine kurulur; yukarıdakilerin hepsi orada da geçerlidir. proxy paketindeki RoundRobinProxySwitcher, http://, https:// ya da socks5:// adresleri alıp Transport.Proxy ile aynı imzaya sahip bir colly.ProxyFunc döndürür; Colly belgelerindeki proxy switcher örneğinde olduğu gibi c.SetProxyFunc(rp) ile bağlanır. Fonksiyon her istekte çağrılır, ama bir isteğin yeni tünel açıp açmayacağına yine rotasyon bölümündeki bağlantı yeniden kullanım kuralı karar verir.

Sık görülen hatalar ve anlamları

Mesajlar Go 1.26.6'nın testlerimizde yazdırdığı hâliyle, yer tutucu adresle:

  • Get "https://httpbin.org/ip": Proxy Authentication Required HTTPS hedefte, düz HTTP hedefte ise normal bir yanıt olarak 407 durum kodu: yanlış user:pass, kodlanmamış özel karakter ya da whitelist'te olmayan bir IP. Nedenleri HTTP 407 Proxy Authentication Required yazısında topladık.
  • proxyconnect tcp: dial tcp pr.proxynet.io:8000: … ve devamında işletim sisteminin bağlantı reddi metni (Linux ve macOS'ta connect: connection refused, Windows'ta connectex: ve yerelleştirilmiş bir cümle): o adreste dinleyen yok. proxyconnect, hatanın hedefe değil proxy'ye giderken oluştuğunu söyler.
  • socks connect tcp pr.proxynet.io:8000->httpbin.org:443: unexpected protocol version 72: HTTP portuna socks5:// adresi verilmiş; 72, HTTP/1.1 içindeki H harfinin bayt değeridir.
  • socks connect tcp …: username/password authentication failed: SOCKS5 sunucusu kimlik bilgisini reddetti.
  • unexpected EOF: SOCKS5 portuna http:// adresi verilmiş, sunucu CONNECT isteğinde bağlantıyı kapatmış; socks4:// gibi bilinmeyen bir şema http sayılır ve aynı şekilde biter.
  • proxyconnect tcp: tls: first record does not look like a TLS handshake: düz HTTP portuna https:// proxy şeması verilmiş; sağlayıcı portun TLS konuştuğunu söylemiyorsa http:// kullanın.
  • Proxy hiç kullanılmıyor: HTTPS_PROXY olmadan HTTP_PROXY, localhost olan ya da NO_PROXY'ye giren hedef, ilk istekten sonra tanımlanan değişkenler ya da Proxy değeri olmadan kurulmuş Transport.

Bu kurulum nerede kullanılır?

  • Birden çok ülkeden fiyat ve stok kontrolü: proxy kullanıcı adında ülke parametresi, bağımsız ürün sayfaları için istek başına rotasyon.
  • Whitelist'teki sabit adresten çıkan API istemcileri: iş ortağı tek bir statik IP'yi whitelist'e almıştır ve servis tam o adresten çıkmak zorundadır.
  • Kendi ağınızın dışından izleme: sağlık kontrolü siteyi başka bir bölgedeki ziyaretçinin gördüğü gibi görür.
  • Kurumsal ağdaki işçiler: internete yalnız şirket proxy'si üzerinden çıkan programlar.

Sık yapılan hatalar

  • Yalnız HTTP_PROXY tanımlamak. HTTPS hedefler doğrudan gider; HTTPS_PROXY de tanımlayın.
  • İlk istekten sonra ortamı değiştirmek. net/http önbellekler; süreci yeniden başlatın ya da httpproxy kullanın.
  • Keep-alive açıkken rotasyon beklemek. Yeniden kullanılan bağlantı Proxy fonksiyonunu bir daha çağırmaz.
  • resp.Body.Close() unutmak. Bağlantılar havuza hiç dönmez.
  • Client.Timeout tanımlamamak. Sessiz bir çıkış goroutine'i sonsuza kadar bekletir.
  • @ ya da : içeren parolayı adres dizesine yapıştırmak. Adresi url.UserPassword ile kurun.
  • Hem Transport.Proxy hem SOCKS5 DialContext ayarlamak. Birini seçin.
  • 407'yi yeniden denemek. İkinci deneme aynı yanlış bilgiyi gönderir.

Hangi yaklaşımı seçmeli?

DurumunuzÖneri
Tek proxy, kod değişmedenHTTPS_PROXY ve HTTP_PROXY, iç host'lar için NO_PROXY
Tek proxy, koddaTransport{Proxy: http.ProxyURL(u)} ve Client.Timeout
Her istekte yeni çıkışKendi Proxy fonksiyonunuz ya da rotating gate, DisableKeepAlives: true
Giriş akışında aynı çıkışKullanıcı adında oturum parametresi, keep-alive açık
DNS'i proxy'de çözen SOCKS5Transport.Proxy içinde socks5://user:pass@host:port
HTTP olmayan bağlantı için SOCKS5golang.org/x/net/proxy dialer'ı
Tek siteye çok işçiMaxConnsPerHost ve MaxIdleConnsPerHost = işçi sayısı
Kendi CA'sı olan kurumsal proxyCA tls.Config.RootCAs içinde, asla InsecureSkipVerify

Sık sorulan sorular

Go'da http.Get proxy'yi otomatik kullanır mı?

Ortam öyle diyorsa evet: http.Get, Transport'unda Proxy alanı http.ProxyFromEnvironment olan http.DefaultClient'ı kullanır. Kendi kurduğunuz Transport ise proxy'siz başlar.

Bu GOPROXY ile aynı şey mi?

Hayır. GOPROXY, go komutuna modülleri nereden indireceğini söyler; programınızın çalışırken gönderdiği isteklerle ilgisi yoktur.

Yalnız tek bir istek için proxy nasıl ayarlanır?

O isteğe kendi Client ve Transport'unu verin ya da doğrudan gitmesi gereken istekler için nil döndüren bir Proxy fonksiyonu yazın. Aynı host'a havuzda açık bir bağlantı olabilir; req.Close = true yeni bağlantıyı zorlar.

Go socks5h destekliyor mu?

Evet, socks5 de aynı şekilde davranır: Go alan adını her zaman proxy'ye gönderir, çözümlemeyi proxy yapar; net/http'de yerel çözümlemeli SOCKS5 modu yoktur.

localhost neden hiç proxy'den geçmiyor?

ProxyFromEnvironment, değişkenler ne derse desin localhost ve loopback adresleri için proxy döndürmez; bu istekleri proxy'den geçirmek için ortam yerine http.ProxyURL kullanın.

Bir isteğin hangi proxy'yi kullandığını nasıl görürüm?

Proxy fonksiyonunu sarıp u.Redacted() değerini kaydedin ya da proxy adresiyle CONNECT yanıtını alan Transport.OnProxyConnectResponse alanını ayarlayın. Keep-alive açıkken seçim yalnız yeni bağlantı açıldığında kaydedilir.

Özet

Go'da proxy, http.Transport üzerindeki bir fonksiyondur: sabit adres için http.ProxyURL, standart değişkenler için http.ProxyFromEnvironment, istek başına seçim için kendi fonksiyonunuz. Adrese user:pass yazın, Proxy-Authorization başlığını Go kendisi gönderir; SOCKS5 için socks5:// kullanın; Client.Timeout ve havuz sınırlarını ayarlayın; 429 ve 5xx'i beklemeyle yeniden deneyin ama 407'yi asla; her istekte yeni çıkış gerekiyorsa keep-alive'ı kapatın. Bu ayarlarla çalışan residential, statik ISP, datacenter ve SOCKS5 uç noktalarını Proxynet proxy sayfasında bulabilirsiniz.

ChatGPT'ye sorClaude'a sor