---
title: "Go (Golang) ile Proxy Kullanımı: net/http Proxy Ayarları"
description: "Go'da proxy, http.Transport'un Proxy alanıyla ayarlanır: http.ProxyURL, user:pass, SOCKS5, zaman aşımı, yeniden deneme ve rotasyon; Go 1.26 ile test edildi."
url: https://proxynet.io/tr/blog/golang-proxy
date: 2026-10-28
author: "Acar Diveroli"
category: "Nasıl Yapılır, Entegrasyon"
lang: tr
---

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

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ı](/tr/blog/nodejs-proxy) yazısında.

> **Not: Kısa cevap**
>
> `http://user:pass@pr.proxynet.io:8000` adresini `url.Parse` ile ayrıştırın, `Proxy: http.ProxyURL(u)` alanıyla bir `http.Transport` kurun ve bunu `Timeout` tanımlı bir `http.Client`'a verin; kullanıcı adı ve parolayı Go kendisi `Proxy-Authorization` başlığı olarak gönderir. SOCKS5 için aynı adreste `socks5://` şemasını kullanın. `http.Get` arkada `http.ProxyFromEnvironment` kullandığından sıradan bir program `HTTPS_PROXY`, `HTTP_PROXY` ve `NO_PROXY` değişkenlerine zaten uyar. Her istekte farklı proxy istiyorsanız kendi `Proxy` fonksiyonunuzu yazın ve keep-alive'ı kapatın; yeniden kullanılan bir bağlantı o fonksiyona bir daha sormaz.

## 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](https://pkg.go.dev/net/http#Transport) 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](https://www.rfc-editor.org/rfc/rfc9110.html#section-9.3.6) 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](https://pkg.go.dev/golang.org/x/net/http/httpproxy) 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](/tr/blog/proxy-authentication-methods) 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](https://www.rfc-editor.org/rfc/rfc1929)'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](https://proxynet.io/tr/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öntem | Nereye yazılır | Kapsam | Not |
|---|---|---|---|
| `http.ProxyURL(u)` | `Transport.Proxy` | o Transport | tek sabit proxy |
| `http.ProxyFromEnvironment` | `Transport.Proxy` (`DefaultTransport` varsayılanı) | süreç | bir kez okunur; `localhost` atlanır |
| kendi `func(*http.Request)` fonksiyonunuz | `Transport.Proxy` | istek başına | rotasyon, host'a göre kural |
| `socks5://` adresi | yukarıdaki üçünden biri | o Transport | DNS proxy'de çözülür |
| `x/net/proxy` dialer'ı | `Transport.DialContext` | o Transport | ham 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ı) | 1 | 1, yalnız ilk proxy |
| Yalnız HTTP/1.1 (boş `TLSNextProto` haritası) | 4 | 2, proxy başına bir, yeniden kullanıldı |
| `DisableKeepAlives: true` | 4 | 4, 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](https://proxynet.io/tr/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](https://go-colly.org/docs/examples/proxy_switcher/) 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](/tr/blog/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şmeden | `HTTPS_PROXY` ve `HTTP_PROXY`, iç host'lar için `NO_PROXY` |
| Tek proxy, kodda | `Transport{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 SOCKS5 | `Transport.Proxy` içinde `socks5://user:pass@host:port` |
| HTTP olmayan bağlantı için SOCKS5 | `golang.org/x/net/proxy` dialer'ı |
| Tek siteye çok işçi | `MaxConnsPerHost` ve `MaxIdleConnsPerHost` = işçi sayısı |
| Kendi CA'sı olan kurumsal proxy | CA `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](/tr/proxy) sayfasında bulabilirsiniz.
