---
title: 'HTTP İstemcisi'
source: 'https://academia.sh/tr/kurslar/go-uygulamalar/http-istemcisi'
course: 'Standart Kütüphane ve Ağ Uygulamaları'
language: tr
updated: '2026-08-23T16:55:13+00:00'
license: 'CC BY-SA 4.0'
---

# HTTP İstemcisi

Önceki ders sınırı alıcı taraftan ölçtü; bu ders aynı sınırı gönderen taraftan ölçüyor. Zaman aşımı bir süreyle değil, iptal edilmiş bir bağlamla kuruluyor. Bağlantının yeniden kullanılıp kullanılmadığı yanıt gövdesinin kapatılıp kapatılmadığına bağlı: gövde okunup kapatılınca sonraki istek aynı bağlantıyı kullanıyor, kapatılmayınca kullanmıyor. Sınırlayıcı ölçüm: yanıt gövdesi bir okuyucudur, bir değer değil — bir kez okunduktan sonra ikinci okuma boş dönüyor.

Önceki ders sınırı **alıcı** taraftan ölçtü: bir isteğin sunucuya ne getirdiğini, ve sunucunun
gövdeye ne kadar körü körüne güvendiğini. Bu ders aynı sınırı **gönderen** taraftan soruyor.
Bir istemci bir isteği gönderirken üç sorumluluğu üstleniyor: isteğin makul bir sürede
sonuçlanmasını sağlamak, aynı sunucuyla konuşan bağlantıları yeniden kullanmak, ve aldığı
yanıtın gövdesini kapatmak. Bu üçü birbirinden bağımsız görünse de, aşağıdaki ölçüm ikisinin
(bağlantı yeniden kullanımı ile gövde kapatma) aslında **aynı kararın** iki yüzü olduğunu
gösteriyor.

Bu ders de bir önceki gibi `net/http/httptest` kullanıyor; ama bu kez `httptest.NewRecorder`
değil, `httptest.NewServer` — istemcinin gerçekten bir bağlantı kurup kapatabileceği,
bellekte çalışan bir sunucu. Sunucunun adresi (`sunucu.URL`) yalnızca istek kurmak için
kullanılıyor, hiçbir ölçüm satırında basılmıyor; ölçülen şey adresin kendisi değil, istemcinin
bu adrese karşı nasıl davrandığı. Bu farkın kendisi ölçümün bir parçası: önceki ders sunucuyu
hiç ağa bağlamadan koşturabiliyordu, çünkü ölçtüğü şey yalnızca isteğin ve yanıtın içeriğiydi;
bu ders istemcinin bağlantı yönetimini ölçtüğü için gerçek (ama hiçbir yerde basılmayan) bir
yerel bağlantıya ihtiyaç duyuyor.

Üç sorumluluk sırayla ele alınıyor. İlki, bir isteğin sonsuza kadar beklemeyeceğinin
garantisi. İkincisi, art arda gelen isteklerin her birinde yeni bir bağlantı kurmanın
maliyetinden kaçınmak. Üçüncüsü, alınan yanıtın gövdesini bırakmadan kapatmak — ve aşağıdaki
ölçüm bu üçüncü sorumluluğun aslında ikinciyi mümkün kılan şart olduğunu gösteriyor.

## Zaman Aşımı: Süre Değil, İptal Edilmiş Bir Bağlam

Önceki kursun bağlam paketi dersinde kurulan kalıp burada aynen sürüyor: bir zaman aşımı
`time.Sleep` ya da gerçek bir süre ölçümüyle değil, önceden **iptal edilmiş bir bağlamla**
kuruluyor. Bu, dersin çıktısının koşumdan koşuma değişmemesini sağlıyor — gerçek bir zaman
aşımı denenseydi, isteğin ne kadar sürede başarısız olacağı makineye ve yüke bağlı kalırdı.

```go
// sunucucekirdek.go — istemci olcumlerinin ortak sunucu govdesi
package main

import (
	"fmt"
	"net/http"
)

// yaniticiIsleyici, her istege sabit bir metin govde donduren bir isleyicidir.
// Govde kucuk tutuluyor ki okunup kapatilmasi olcumu etkilemesin.
func yaniticiIsleyici(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/plain")
	fmt.Fprint(w, "adet=7 birim=kg")
}

// olcumSatiri, sinirdaki tip bilgisinin uc sonucundan birini (korunuyor / yeniden /
// kayboluyor) bir kanitla birlikte yazdirir.
func olcumSatiri(parca, sinif, kanit string) {
	fmt.Printf("%-30s %-10s %s\n", parca, sinif, kanit)
}
```

```go
// zamanasimi.go — zaman asimi sureyle degil, iptal edilmis bir baglamla kuruluyor
package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- zaman asimi: sureyle degil, iptal edilmis bir baglamla --")

	sunucu := httptest.NewServer(http.HandlerFunc(yaniticiIsleyici))
	defer sunucu.Close()

	baglam, iptalEt := context.WithCancel(context.Background())
	iptalEt() // istek daha baslamadan baglam zaten iptal edildi

	istek, _ := http.NewRequestWithContext(baglam, http.MethodGet, sunucu.URL, nil)
	_, hata := http.DefaultClient.Do(istek)

	fmt.Printf("istek hatasi var mi=%t\n", hata != nil)
	fmt.Printf("hata baglam iptaliyle mi eslesiyor=%t\n", errors.Is(hata, context.Canceled))
}
```

```
-- zaman asimi: sureyle degil, iptal edilmis bir baglamla --
istek hatasi var mi=true
hata baglam iptaliyle mi eslesiyor=true
```

**AG7.** `iptalEt()` çağrısı, isteğin gönderilmesinden **önce** çalışıyor; bu sıralama
bilinçli, çünkü ölçümün amacı zamanlamayla ilgili hiçbir tartışmaya yer bırakmamak. Bağlam
zaten iptal edilmiş durumdayken `http.DefaultClient.Do` çağrılıyor, ve istemci isteği hiç
kabloya koymadan hemen hata döndürüyor. `errors.Is(hata, context.Canceled)` denetimi
`true` çıkıyor — dönen hata, ağ katmanının kendi ürettiği bir hata değil, bağlamın
taşıdığı sabit hatanın sarmalanmış hâli. Bir gerçek zaman aşımı (`context.WithTimeout`)
kurulsaydı aynı mekanizma işlerdi, yalnızca hata `context.Canceled` yerine
`context.DeadlineExceeded` olurdu; ikisi de aynı `ctx.Err()` yolundan geliyor.

**AG8.** Bu ölçüm, önceki kursun kapanışında bırakılan soruyu bu sınırda yeniden soruyor:
bir goroutine'i durduran şey neydi — kendi kod yolundaki bir kontrol noktası. `http.Client`
da aynı ilkeyi izliyor: bağlamın iptalini kendi iç döngüsünde (bağlantı kurulurken,
yanıt beklerken) denetliyor ve bu kontrol noktasına ulaştığında durup hatayı döndürüyor.
Fark, bu kez kontrol noktasının kaynak kodda görünmemesi — istemcinin kendisi standart
kitaplığın bir parçası, ama davranış aynı: bağlam bir zorlama aracı değil, bir haber
kaynağı, ve `http.Client` bu haberi dinleyen taraflardan biri. Bu tutarlılık, standart
kitaplığın kendi iç işlevlerini de aynı ilkeye tabi tutmasından geliyor; bağlam paketi bir
kez öğrenildiğinde, sınırın hangi tarafında olursa olsun aynı biçimde davranıyor.

Bu ölçümde gerçek bir süre hiç kullanılmadı; `context.WithCancel` ile `iptalEt()`in hemen
çağrılması, isteğin başarısız olacağını **kesin** kılıyor, "belki zamanında yetişir belki
yetişmez" belirsizliğini tamamen ortadan kaldırıyor. Gerçek bir `context.WithTimeout`
kullanılsaydı, ölçümün sonucu doğrulayıcının çalıştığı makinenin hızına bağlı kalırdı — aynı
kaynak kimi koşumda zaman aşımına uğrar, kimi koşumda sunucu yeterince hızlı yanıt verdiği
için uğramazdı. Önceden iptal edilmiş bir bağlam bu belirsizliği koşum zamanından tasarım
zamanına taşıyor.

## Bağlantı Yeniden Kullanımı: Gövdeyi Kapatmanın Bedeli

Bir TCP bağlantısı kurmak ucuz bir işlem değil; Go'nun `http.Transport`'u bu yüzden
kullanılmış bağlantıları bir **bağlantı havuzunda** tutup sonraki isteklerde yeniden
kullanmaya çalışıyor. Bağlantı havuzu kavramının kendisi Veri Erişim Katmanı kursunda
kuruldu; burada ölçülen şey Go'nun standart kitaplığındaki `http.Transport`'un bu havuzu
hangi koşulda işlettiği. Bu yeniden kullanım koşulsuz değil: bir yanıtın gövdesi tamamen
okunup kapatılmadan, altındaki bağlantı havuza geri dönmüyor. Bu, önceki bölümdeki zaman
aşımından bağımsız bir sorumluluk — istek başarıyla tamamlansa, durum kodu `200` dönse bile,
gövdeyi görmezden gelmek bağlantıyı **kaybettirebiliyor**.

Aşağıdaki ölçüm bunu `net/http/httptrace` ile gözlemliyor: `httptrace.ClientTrace`in
`GotConn` alanı, her istek için elde edilen bağlantının **yeniden kullanılmış** olup
olmadığını (`Reused` alanı, bir `bool`) bildiriyor. Bu paket de standart kitaplığın bir
parçası ve hiçbir gerçek adres ya da bağlantı noktası açığa çıkarmıyor, yalnızca bir mantıksal
bayrak veriyor.

Bir önceki bölüm zaman aşımını ölçerken `http.DefaultClient`i kullanmıştı, çünkü orada
ölçülen şey bağlam iptaliydi ve hangi istemcinin kullanıldığı sonucu değiştirmiyordu. Bu
bölüm kendi istemcilerini açıkça kuruyor, çünkü burada ölçülen şey tam olarak istemcinin
taşıdığı bağlantı havuzu — `http.DefaultClient`i kullanmak, bu ölçümü kursun başka bir
dersinde çalışan kodun bıraktığı bağlantılarla karışabilir hâle getirirdi. Bu, bir HTTP
istemcisinin görünmeyen bir durum (açık bağlantılar) taşıdığının ilk işareti: iki farklı
`*http.Client` değeri, aynı sunucuya karşı bile olsa, birbirinden tamamen farklı bir geçmiş
biriktirebiliyor.

```go
// baglantitekrar.go — govdenin kapatilmasi baglantinin havuza donup donmedigini belirliyor
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/http/httptrace"
)

// izleyerekIste, bir istegi verilen istemciyle gonderir; baglantinin yeniden mi
// kullanildigini httptrace ile ogrenip govdeyi istege gore kapatir.
func izleyerekIste(istemci *http.Client, url string, govdeyiKapat bool) (yenidenKullanildi bool) {
	baglam := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
		GotConn: func(bilgi httptrace.GotConnInfo) { yenidenKullanildi = bilgi.Reused },
	})
	istek, _ := http.NewRequestWithContext(baglam, http.MethodGet, url, nil)
	yanit, hata := istemci.Do(istek)
	if hata != nil {
		return false
	}
	if govdeyiKapat {
		io.ReadAll(yanit.Body)
		yanit.Body.Close()
	}
	return yenidenKullanildi
}

func main() {
	fmt.Println("-- baglanti yeniden kullanimi: govde kapatildiginda ve kapatilmadiginda --")

	sunucu := httptest.NewServer(http.HandlerFunc(yaniticiIsleyici))
	defer sunucu.Close()

	// Her istemci kendi Transport'unu tasiyor; boylece baglanti havuzlari birbirinden
	// bagimsiz oluyor ve bir istemcideki kapatilmamis govde otekini etkilemiyor.
	istemciA := &http.Client{Transport: &http.Transport{}}
	ilkA := izleyerekIste(istemciA, sunucu.URL, true)
	ikinciA := izleyerekIste(istemciA, sunucu.URL, true)
	fmt.Printf("govde her seferinde kapatildi: ilk=%t ikinci=%t\n", ilkA, ikinciA)

	istemciB := &http.Client{Transport: &http.Transport{}}
	izleyerekIste(istemciB, sunucu.URL, false)
	ikinciB := izleyerekIste(istemciB, sunucu.URL, true)
	fmt.Printf("ilk istegin govdesi hic kapatilmadi: ikinci=%t\n", ikinciB)
}
```

```
-- baglanti yeniden kullanimi: govde kapatildiginda ve kapatilmadiginda --
govde her seferinde kapatildi: ilk=false ikinci=true
ilk istegin govdesi hic kapatilmadi: ikinci=false
```

**AG9.** `istemciA` ile yapılan iki istekte gövde her seferinde okunup kapatılıyor. İlk
istekte henüz açık bir bağlantı olmadığı için `Reused=false` çıkması beklenen bir sonuç; asıl
ölçülen şey **ikinci** istek. Gövde ilk istekte tam okunup kapatıldığı için bağlantı havuza
geri dönmüş oluyor, ve ikinci istek `Reused=true` ile aynı bağlantıyı devralıyor. Bu, istemci
tarafının üçüncü sorumluluğunun (gövde kapatma) neden yalnızca "temizlik" değil, doğrudan
başarım kararı olduğunu gösteriyor.

`istemciA` ve `istemciB`nin her biri kendi `*http.Transport` değerini taşıyor; bu bilinçli
bir seçim. `Transport` alanı boş bırakılsaydı, ikisi de standart kitaplığın **paylaşılan**
varsayılan aktarımını kullanırdı ve iki istemcinin bağlantı havuzları aslında aynı havuz
olurdu — `istemciB`nin kapatılmamış gövdesi, adı farklı olsa bile `istemciA`nın sonraki
isteğini de etkileyebilirdi. Ayrı birer `Transport` vermek, iki senaryoyu birbirinden
**yalıtıyor**: ölçülen etki gerçekten `istemciB`nin kendi kapatılmamış gövdesinden geliyor,
başka bir isteğin yan etkisinden değil.

**AG10.** `istemciB` ile yapılan ilk istekte gövde hiç okunmuyor, hiç kapatılmıyor. İkinci
istek aynı istemciyle, gövdesi düzgün kapatılarak gönderiliyor, ama sonuç yine
`Reused=false`. Sebep, ilk isteğin bağlantısının hâlâ **meşgul** sayılması: `Transport`,
gövdesi okunup kapatılmamış bir yanıtın altındaki bağlantıyı asla havuza koymuyor, çünkü o
bağlantının ne zaman serbest kalacağını bilmiyor. İkinci istek bu yüzden yeni bir bağlantı
açmak zorunda kalıyor — birinci isteğin kapatılmamış gövdesi, kendisinden sonraki isteği de
etkiliyor.

Bu etkinin ölçekle büyüdüğü açık: art arda yüzlerce istek gönderen, ama gövdelerinin bir
kısmını kapatmayı unutan bir program, her kapatılmamış gövde için bir bağlantıyı sonsuza
kadar meşgul tutuyor. Programın kendisi çökmüyor, hata da vermiyor — yalnızca istemcinin
açık bağlantı sayısı zamanla birikiyor, ve bu birikim gövdenin kendisi kadar sessiz kalıyor.

**AG11.** Bu iki ölçüm birlikte okunduğunda ortaya çıkan kural net: bağlantı yeniden
kullanımı **kimin işi** sorusuna tek bir yanıt veriyor — çağıranın işi. Standart kitaplık
gövdeyi otomatik olarak okuyup kapatmıyor; `Body.Close()` çağrılmazsa, hatta çağrılsa bile
gövde önce tam okunmazsa, bağlantı sızıyor ya da en azından yeniden kullanılamıyor. Bu,
önceki dersteki ara katman zincirinde görülen ilkeyle aynı yerde duruyor: standart kitaplık
sınırın bu tarafında bir mekanizma sunuyor, ama mekanizmayı doğru kullanmak yazan tarafın
sorumluluğunda kalıyor.

Bu kural, tip sisteminin sınırdaki rolüne de bir örnek ekliyor: `yanit.Body`nin tipi
(`io.ReadCloser`) kendisi hiçbir yerde "beni tam okumadan kapatma" demiyor. Derleyici,
gövdesi okunmadan bırakılan bir yanıtı reddetmiyor — kaynak sorunsuz derleniyor, sorunsuz
çalışıyor, yalnızca bağlantı havuzu sessizce küçülüyor. Bu tür bir sözleşme (bir kaynağın
kapatılmadan önce tüketilmesi gerektiği) tipin kendisinde değil, belgesinde ve yazan tarafın
alışkanlığında yaşıyor.

## Sınırlayıcı Ölçüm: Gövde Bir Okuyucudur, Bir Değer Değil

Önceki iki ölçüm gövdenin kapatılmasının **neden** önemli olduğunu gösterdi. Bu ölçüm
gövdenin **ne olduğunu** netleştiriyor: `yanit.Body`, `io.ReadCloser` arayüzünü
gerçekleştiren bir akış, isteğin gövdesi gibi bir kez tüketiliyor ve tüketildikten sonra
geriye hiçbir şey kalmıyor. Bu, dilin önceki kurslarında görülen kopyalanabilir değerlerden
(bir dilim, bir yapı) temelden farklı bir davranış: bir dilimi iki değişkene atayıp ikisini de
ayrı ayrı okumak mümkündü, çünkü dilim bir değerdi. Bir okuyucu değer değil, bir **konum**
taşıyor — okundukça o konum ilerliyor ve geriye dönmüyor.

```go
// govdeokuma.go — yanit govdesi bir okuyucudur, bir deger degil
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	fmt.Println("-- yanit govdesi bir okuyucudur, iki kez okunamaz --")

	sunucu := httptest.NewServer(http.HandlerFunc(yaniticiIsleyici))
	defer sunucu.Close()

	yanit, _ := http.Get(sunucu.URL)
	defer yanit.Body.Close()

	ilkOkuma, _ := io.ReadAll(yanit.Body)
	ikinciOkuma, ikinciHata := io.ReadAll(yanit.Body)

	olcumSatiri("yanit govdesi (istemcide okunan)", "kayboluyor",
		fmt.Sprintf("tip=%T ilk-uzunluk=%d", ilkOkuma, len(ilkOkuma)))
	fmt.Printf("ikinci okuma uzunlugu=%d hata=%v\n", len(ikinciOkuma), ikinciHata)
}
```

```
-- yanit govdesi bir okuyucudur, iki kez okunamaz --
yanit govdesi (istemcide okunan) kayboluyor tip=[]uint8 ilk-uzunluk=15
ikinci okuma uzunlugu=0 hata=<nil>
```

**AG12.** İlk `io.ReadAll` çağrısı gövdeyi baştan sona okuyup on beş baytlık bir dilim
döndürüyor — sınırın bu tarafına ulaşan şey yine `[]byte`, önceki derste görülen aynı sınıf:
**kayboluyor**. İkinci `io.ReadAll` çağrısı ise `0` uzunlukta bir sonuç ve `nil` bir hata
döndürüyor; okunacak hiçbir şey kalmadığı için okuma işlemi anında bitiyor, ama bu bir hata
sayılmıyor. Gövde bir dilim ya da bir dizgi olsaydı, aynı değeri istendiği kadar okumak mümkün
olurdu; bir okuyucu olduğu için tüketildiği an geride yalnızca boşluk kalıyor.

**AG13.** Bu davranışın pratik sonucu: bir yanıt gövdesi ancak **bir kez** ayrıştırılabilir.
Bir işlev gövdeyi hem günlüğe yazmak hem de bir hedefe çözmek istiyorsa, gövdeyi önce bir
arabelleğe (`bytes.Buffer` gibi) kopyalayıp iki farklı okumayı o arabellek üzerinden yapmak
zorunda — kaynağın kendisine ikinci kez dönmek mümkün değil. Bu sınırlama, bir önceki
bölümdeki bağlantı yeniden kullanımıyla da bağlantılı: gövdeyi bir arabelleğe kopyalamak,
onu aynı zamanda tam olarak tüketmek anlamına geliyor, dolayısıyla bu kopyalama işlemi hem
ikinci bir okumayı mümkün kılıyor hem de altındaki bağlantının havuza dönmesini sağlıyor —
aynı `io.ReadAll` çağrısı iki ayrı sorunu birden çözüyor.

Kursun on üç sınır geçişinin tam dağılımı (1 korunuyor, 5 yeniden kuruluyor, 7 kayboluyor)
sıradaki derste, JSON Dizileştirme'de kuruluyor; bu ders yalnız istemci tarafının üç
sorumluluğunu ve gövdenin okuyucu doğasını ölçtü.

## Özet

- Zaman aşımı bir süre yerine önceden iptal edilmiş bir bağlamla kuruluyor; `http.Client`
  bağlamın iptalini kendi iç kontrol noktalarında denetleyip isteği bu noktada durduruyor.
- `errors.Is` ile denetlenen istek hatası, bağlamın taşıdığı sabit hatanın
  (`context.Canceled` ya da `context.DeadlineExceeded`) sarmalanmış hâli.
- Bir bağlantının yeniden kullanılıp kullanılmayacağı `httptrace.GotConnInfo.Reused`
  alanıyla ölçülüyor; gövde tam okunup kapatılmazsa bağlantı havuza dönmüyor.
- Gövdesi kapatılmamış bir isteğin etkisi kendisinden sonraki isteğe de yansıyor: bir
  önceki bağlantı meşgul sayıldığı için yeni bir bağlantı açılıyor.
- Yanıt gövdesi bir okuyucudur, bir değer değil: bir kez tam okunduktan sonra ikinci okuma
  boş dönüyor, hata vermeden.
- Bu ders sınırı gönderen taraftan ölçtü; kursun on üç sınır geçişinin tam dağılımı henüz
  burada kurulmadı, sıradaki ders kuruyor.

## Sonraki Adım

İki ders de sınırın **taşıyıcısını** ölçtü — istek ve yanıt bayt ve metin taşıyor, gövde bir
kez okunabiliyor. Bu iki dersin ikisinde de gövdenin **içeriği** hiç önemli değildi;
`"adet=3"` ya da `"adet=7 birim=kg"` yerine rastgele herhangi bir metin de aynı sonuçları
verirdi, çünkü ölçülen şey gövdenin davranışıydı, taşıdığı veri değil. Ama hiçbiri şu soruyu
henüz sormadı: gönderen taraf bir **yapıyı** gönderirken karşı tarafın onu geri kurabilmesi
için ne bırakıyor? Sıradaki ders bu soruyu JSON ile ölçüyor ve kursun bağlayıcı sayısını
üretiyor: bir yapının alan adları yapı etiketleriyle karşıya taşınıyor, ama bu taşımanın
sınırları var — alıcıda karşılığı olmayan bir alan sessizce düşüyor, ve "geri kuruldu"
görünmesi her zaman "doğru kuruldu" anlamına gelmiyor.
