Go'da HTTP isteği göndermenin en kısa yolu http.Get(url) çağrısıdır; dönen yanıtın Body alanını okuyup mutlaka kapatırsınız. Üretim kodunda ise Timeout alanı ayarlanmış tek bir http.Client oluşturup tekrar kullanır, başlık ve bağlam gerektiren istekleri http.NewRequestWithContext ve client.Do ile gönderirsiniz. Akılda tutulması gereken en önemli kural şudur: 404 ya da 500 gibi 2xx dışı durum kodları hata sayılmaz, err nil gelir ve StatusCode değerini sizin kontrol etmeniz gerekir.
Bu yazıda net/http paketinin istemci tarafını anlatıyoruz: yanıt gövdesinin yaşam döngüsü, zaman aşımı türleri, *url.Error ile hata ayırt etme, JSON gövdeli POST isteği, sorgu dizesi oluşturma ve yönlendirme politikası. Örneklerde dış bir siteye gitmek yerine aynı program içinde httptest.NewServer ile yerel bir sunucu açıyoruz; böylece her örnek ağ bağlantısı olmadan çalışır ve sonuç tekrarlanabilir. Sunucu tarafı için Go net/http web sunucusu yazısına bakın.
http.Get ve yanıt gövdesinin kapatılması
http.Get, paket genelindeki http.DefaultClient değerinin Get metodunu çağırır. Başarılı bir çağrıda resp.Body her zaman nil olmayan bir akıştır; gövde sunucudan siz okudukça gelir. Belgelere göre gövdeyi kapatmak çağıranın sorumluluğudur ve gövde sonuna kadar okunup kapatılmazsa alttaki Transport aynı TCP bağlantısını sonraki istekte yeniden kullanamayabilir. Go 1.27 sürüm notları, HTTP/1 yanıt gövdesinin kapatılırken okunmamış içeriği temkinli bir sınıra kadar otomatik olarak boşalttığını, bunun çoğu program için ya etkisiz ya da bağlantı yeniden kullanımını iyileştiren bir değişiklik olduğunu belirtir.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /selam", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "selam istemci")
})
ts := httptest.NewServer(mux)
defer ts.Close()
for _, yol := range []string{"/selam", "/olmayan"} {
resp, err := http.Get(ts.URL + yol)
if err != nil {
fmt.Println("ağ/protokol hatası:", err)
continue
}
govde, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Println("gövde okunamadı:", err)
continue
}
// 404 bir hata değildir: err nil gelir, durumu siz kontrol edersiniz.
if resp.StatusCode != http.StatusOK {
fmt.Printf("%s: beklenmeyen durum %d\n", yol, resp.StatusCode)
continue
}
fmt.Printf("%s: %s\n", yol, govde)
}
}
// Çıktı:
// /selam: selam istemci
// /olmayan: beklenmeyen durum 404Örnekteki ikinci istek 404 aldı ama err nil geldi. Belgeler bunu açıkça söyler: hata yalnızca istemci politikası (örneğin yönlendirme kontrolü) ya da HTTP konuşulamaması (ağ hatası gibi) durumunda döner. Bu yüzden her yanıtta durum kodunu kontrol eden küçük bir yardımcı fonksiyon yazmak iyi bir alışkanlıktır. defer resp.Body.Close() ifadesini hata kontrolünden sonra yazmalısınız; hata varken resp nil olabileceği için önce yazılan defer panic üretir.
Neden tek bir http.Client tekrar kullanılır?

http.Client çerez, yönlendirme ve zaman aşımı gibi üst düzey ayrıntıları yönetir; asıl bağlantı işini Transport alanındaki RoundTripper yapar. Belgelere göre Transport önbelleğe alınmış TCP bağlantıları gibi iç durum taşıdığından istemciler ihtiyaç anında yeniden oluşturulmamalı, tekrar kullanılmalıdır. Hem Client hem Transport birden fazla goroutine tarafından eşzamanlı kullanım için güvenlidir. Pratikte uygulama başlarken bir istemci kurup ilgili servislere paylaştırırsınız; her istekte &http.Client{} oluşturmak bağlantı havuzundan yararlanmayı engeller.
| Ayar | Nerede | Ne işe yarar |
|---|---|---|
| Timeout | http.Client | Bağlantı, yönlendirmeler ve gövde okuma dahil toplam süre sınırı |
| CheckRedirect | http.Client | Yönlendirme politikası; nil ise 10 ardışık istekten sonra durur |
| Jar | http.Client | Çerez kavanozu; nil ise yalnızca isteğe elle eklenen çerezler gider |
| Transport | http.Client | Bağlantıları yapan RoundTripper; nil ise DefaultTransport |
| MaxIdleConns, IdleConnTimeout | http.Transport | Boşta bekleyen bağlantı havuzunun boyutu ve ömrü |
| context | http.Request | Tek bir isteğin tüm yaşam süresini iptal veya süre ile sınırlar |
Zaman aşımı: Client.Timeout

http.DefaultClient değerinin Timeout alanı sıfırdır; belgelere göre sıfır, zaman aşımı yok demektir. Yanıt vermeyen bir sunucu, varsayılan istemciyi kullanan bir goroutine'i süresiz bekletebilir. Client.Timeout bağlantı kurmayı, yönlendirmeleri ve yanıt gövdesini okumayı kapsar; ayrıca zamanlayıcı Get veya Do döndükten sonra da çalışmaya devam eder ve süre dolarsa Body okumasını keser. Dönen her hata *url.Error tipindedir ve Timeout() metodu isteğin süre aşımıyla bitip bitmediğini söyler.
package main
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(2 * time.Second): // yavaş bir sunucu
case <-r.Context().Done(): // istemci vazgeçerse beklemeyi bırak
}
}))
defer ts.Close()
client := &http.Client{Timeout: 200 * time.Millisecond}
_, err := client.Get(ts.URL)
var uerr *url.Error
if errors.As(err, &uerr) {
fmt.Println("işlem:", uerr.Op)
fmt.Println("zaman aşımı mı:", uerr.Timeout())
}
}
// Çıktı:
// işlem: Get
// zaman aşımı mı: trueBu örnekte err.Error() metni bu makinede Get "http://127.0.0.1:PORT": context deadline exceeded (Client.Timeout exceeded while awaiting headers) şeklindeydi; port her çalıştırmada değiştiği için metni değil Op ve Timeout() değerlerini yazdırdık. Hata mesajlarını string olarak karşılaştırmak yerine Go hata yönetimi yazısında anlattığımız errors.As ve errors.Is fonksiyonlarını kullanın.
NewRequestWithContext ile POST, başlıklar ve JSON
Özel başlık, farklı bir metot ya da bağlam gerektiğinde isteği kendiniz kurarsınız. http.NewRequest, bağlam olarak context.Background kullanan bir kısayoldur; http.NewRequestWithContext ise verdiğiniz bağlamı isteğe bağlar. Belgelere göre giden bir istekte bağlam, bağlantı edinmekten yanıt başlıklarını ve gövdesini okumaya kadar isteğin tüm ömrünü kontrol eder. Gövde *bytes.Buffer, *bytes.Reader ya da *strings.Reader ise ContentLength tam değerine ayarlanır ve GetBody doldurulur; böylece 307 ve 308 yönlendirmelerinde gövde yeniden gönderilebilir.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
)
type siparis struct {
Urun string `json:"urun"`
Adet int `json:"adet"`
Durum string `json:"durum,omitempty"`
}
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer gizli-anahtar" {
http.Error(w, "yetkisiz", http.StatusUnauthorized)
return
}
var s siparis
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
http.Error(w, "bozuk JSON", http.StatusBadRequest)
return
}
s.Durum = "alındı"
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(s)
}))
defer ts.Close()
veri, _ := json.Marshal(siparis{Urun: "Kalem", Adet: 3})
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, ts.URL+"/siparis", bytes.NewReader(veri))
if err != nil {
fmt.Println(err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer gizli-anahtar")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Println("istek başarısız:", err)
return
}
defer resp.Body.Close()
var cevap siparis
if err := json.NewDecoder(resp.Body).Decode(&cevap); err != nil {
fmt.Println("yanıt çözülemedi:", err)
return
}
fmt.Println(resp.StatusCode, req.ContentLength)
fmt.Printf("%+v\n", cevap)
}
// Çıktı:
// 201 25
// {Urun:Kalem Adet:3 Durum:alındı}Burada iki ayrıntıya dikkat edin. Birincisi, ContentLength değerinin 25 olması gövdenin bytes.Reader olmasından kaynaklanır; başka bir io.Reader verseydiniz uzunluk bilinmeyen (-1) kalırdı. İkincisi, istek gövdesini kapatma işini siz yapmazsınız: belgelere göre Do, gövdeyi hata durumunda bile alttaki Transport aracılığıyla kapatır. JSON kodlama ve etiketler için Go encoding/json yazısına bakabilirsiniz.
Örneği baştan sona bir kez daha okuyalım, çünkü gerçek bir API istemcisinin iskeleti budur. Önce gönderilecek veri bir struct olarak hazırlanıp json.Marshal ile bayt dilimine çevrildi. Sonra üç saniyelik bir bağlam oluşturuldu ve defer cancel() ile bağlamın kaynakları serbest bırakıldı. İstek NewRequestWithContext ile kuruldu, içerik türü ve yetki başlığı eklendi. Uzun ömürlü istemcinin genel zaman aşımı on saniye, bu isteğinki üç saniye; hangisi önce dolarsa istek onunla biter. Yanıt geldikten sonra önce hata, sonra durum kodu kontrol edilmeli, gövde de json.NewDecoder ile akış halinde çözülmelidir. Gerçek bir istemcide burada durum kodu 201 değilse gövdeyi hata mesajı olarak okuyup anlamlı bir hata döndürürsünüz.
Bağlamla iptal ve sorgu dizesi oluşturma
Client.Timeout istemcideki her isteğe aynı sınırı uygular. İstek bazında farklı bir süre, kullanıcı vazgeçtiğinde iptal ya da gelen bir sunucu isteğinin bağlamını aşağıya taşımak istiyorsanız bağlam kullanırsınız. Bağlamın süresi dolduğunda dönen hata context.DeadlineExceeded değerini sarar; errors.Is ile yakalanır ve *url.Error yine Timeout() için true döner. Aynı örnekte sorgu dizesini url.Values ile kuruyoruz; değerdeki Türkçe karakter, boşluk ve & işareti doğru şekilde kodlanıyor.
package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"
)
func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(2 * time.Second):
case <-r.Context().Done():
}
}))
defer ts.Close()
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", ts.URL, nil)
_, err := http.DefaultClient.Do(req)
fmt.Println(errors.Is(err, context.DeadlineExceeded))
var uerr *url.Error
fmt.Println(errors.As(err, &uerr), uerr.Op, uerr.Timeout())
// Sorgu dizesini elle birleştirmek yerine url.Values kullanın.
q := url.Values{}
q.Set("ara", "çay & kahve")
q.Set("sayfa", "2")
fmt.Println(ts.URL + "/ara?" + q.Encode())
}
// Çıktı (port her çalıştırmada değişir):
// true
// true Get true
// http://127.0.0.1:PORT/ara?ara=%C3%A7ay+%26+kahve&sayfa=2Sorgu dizesini string birleştirmeyle kurmak, değer içinde & ya da = olduğunda parametreleri bozar. url.Values anahtarları sıralayarak kodlar, bu yüzden Encode çıktısı deterministiktir. Bağlam türleri ve iptalin goroutine'ler arasında yayılması Go context paketi yazısında ayrıntılı olarak anlatılıyor.
Yönlendirmeler ve CheckRedirect
İstemci 301, 302, 303, 307 ve 308 yanıtlarını varsayılan olarak izler ve 10 ardışık istekten sonra durur. Belgelere göre 301, 302 ve 303 yönlendirmeleri sonraki isteği gövdesiz GET'e (orijinal HEAD ise HEAD'e) çevirir; 307 ve 308 ise metodu ve gövdeyi korur, bunun için GetBody tanımlı olmalıdır. Authorization ve Cookie gibi hassas başlıklar, ilk alan adıyla aynı olmayan ya da onun alt alan adı olmayan bir hedefe yönlendirmede gönderilmez. Yönlendirmeyi izlemek istemiyorsanız CheckRedirect fonksiyonundan http.ErrUseLastResponse döndürürsünüz; bu durumda son yanıt gövdesi kapatılmadan ve hatasız döner.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/eski", http.RedirectHandler("/yeni", http.StatusMovedPermanently))
mux.HandleFunc("/yeni", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "yeni adres")
})
ts := httptest.NewServer(mux)
defer ts.Close()
// Varsayılan: istemci yönlendirmeyi kendisi izler (en fazla 10 adım).
resp, err := http.Get(ts.URL + "/eski")
if err != nil {
fmt.Println(err)
return
}
resp.Body.Close()
fmt.Println(resp.StatusCode, resp.Request.URL.Path)
// Yönlendirmeyi izlemeden son yanıtı almak için ErrUseLastResponse.
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err = client.Get(ts.URL + "/eski")
if err != nil {
fmt.Println(err)
return
}
resp.Body.Close()
fmt.Println(resp.StatusCode, resp.Header.Get("Location"))
}
// Çıktı:
// 200 /yeni
// 301 /yeniİlk istekte resp.Request.URL.Path değerinin /yeni olması, son yanıtın hangi istekten geldiğini gösterir. Go 1.26 sürüm notlarına göre Client, mevcut olduğunda Request.Host ile eşleşen URL'lere kapsamlı çerezleri kullanır; önceden her zaman bağlantı adresinin host kısmı kullanılıyordu.
Yanıt başlıkları, çerezler ve Transport ayarları
resp.Header bir http.Header haritasıdır (map[string][]string). Belgeye göre anahtarlar kanonik biçimdedir: CanonicalHeaderKey ilk harfi ve tireden sonra gelen her harfi büyük, kalanları küçük yapar; accept-encoding anahtarı Accept-Encoding olur. Bu yüzden haritayı doğrudan indekslemek yerine resp.Header.Get("content-type") gibi büyük-küçük harf duyarsız okuma yapan Get metodunu kullanmak daha güvenlidir. Aynı başlık birden çok kez gelmişse değerler virgülle birleştirilmiş olabilir. Sunucunun gönderdiği çerezleri resp.Cookies() ile, yönlendirme adresini resp.Location() ile okursunuz.
İstek tarafında req.Header.Set bir başlığın değerini değiştirir, req.Header.Add aynı ada ikinci bir değer ekler. Temel kimlik doğrulama için başlığı elle kodlamak yerine req.SetBasicAuth(kullanici, parola) kullanılır. Form gönderimi için http.PostForm, gövdeyi application/x-www-form-urlencoded olarak kodlar ve Content-Type başlığını kendisi ayarlar.
Bağlantı düzeyindeki ayarlar http.Transport üzerindedir. Paket belgesindeki örnek, boşta tutulacak en fazla bağlantı sayısını MaxIdleConns, boştaki bağlantının ne kadar yaşayacağını IdleConnTimeout, sıkıştırmanın kapatılmasını DisableCompression ile gösterir. Transport sunucudan sıkıştırılmış gelen bir yanıtı kendisi açtıysa resp.Uncompressed true olur, ContentLength -1 yapılır ve Content-Encoding başlığı silinir. Belgeye göre DefaultTransport HTTPS üzerinde HTTP/2 desteğini otomatik açar, sıfırdan oluşturduğunuz bir Transport ise varsayılan olarak açmaz. Vekil sunucu ayarları için ProxyFromEnvironment, HTTP_PROXY, HTTPS_PROXY ve NO_PROXY ortam değişkenlerini okur; hedef localhost ise vekil kullanılmaz.
Sık yapılan hatalar

- Body kapatmamak: özellikle döngü içinde yapılan isteklerde bağlantılar yeniden kullanılamaz ve kaynaklar birikir. Döngüde
deferyerine okuduktan hemen sonraCloseçağırın. - err nil diye isteği başarılı saymak: 4xx ve 5xx yanıtlar hata döndürmez;
StatusCodekontrolü şarttır. - Zaman aşımı koymamak:
http.Getvehttp.DefaultClientsınırsız bekleyebilir. - Her istekte yeni Client veya Transport oluşturmak: bağlantı havuzu boşa gider; Go 1.27 notları da farklı istekler için farklı
Clientkullanmayı hatalı yapılandırma örneği olarak anar. - Hata metnini string olarak karşılaştırmak: mesajlar URL ve port içerir;
errors.Isveerrors.Askullanın. - Sorgu dizesini elle birleştirmek: özel karakterler kodlanmaz;
url.Valueskullanın. - Gövdeyi sınırsız okumak: güvenilmeyen bir sunucudan gelen yanıtı
io.ReadAllile okurkenio.LimitReaderile bir üst sınır koymak belleği korur.
Sık Sorulan Sorular
http.Get ile client.Get arasında fark var mı?
http.Get, DefaultClient.Get için bir sarmalayıcıdır. Davranış aynıdır; fark, varsayılan istemcinin zaman aşımı olmaması ve paket genelinde paylaşılmasıdır. Kendi ayarlarınızı istiyorsanız kendi istemcinizi kurun.
Yanıt gövdesini okumadan kapatabilir miyim?
Evet, gövdeye ihtiyacınız yoksa yalnızca Close çağırmanız yeterlidir. Client.Do belgesine göre Transport, kapatılan gövdeyi temkinli bir sınıra kadar arka planda sonuna kadar okumayı dener; Go 1.27 ile bu boşaltma HTTP/1 için kapatma sırasında otomatik yapılır.
Aynı anda birçok istek göndermek güvenli mi?
Evet. Client ve Transport belgelere göre birden fazla goroutine tarafından eşzamanlı kullanılabilir; tek bir istemciyi paylaşan goroutine'ler bağlantı havuzundan birlikte yararlanır. Kaç isteğin aynı anda uçuşta olacağını sınırlamak ve sonuçları toplamak için sync.WaitGroup ve kanal desenlerini kullanabilirsiniz; her goroutine yine kendi yanıt gövdesini kapatmalıdır.
Kendi kendine imzalı sertifikalı bir test sunucusuna nasıl bağlanırım?
Testlerde httptest.NewTLSServer ile açtığınız sunucunun Client() metodu, sunucunun test sertifikasına güvenecek şekilde ayarlanmış bir istemci döndürür. Gerçek ortamda sertifika doğrulamasını kapatmak yerine doğru sertifika zincirini kullanın; konu için TLS 1.3 rehberi yazısına bakın.
İstemcimi nasıl test ederim?
Bu yazıdaki gibi httptest.NewServer ile sahte bir sunucu açıp istemcinizi onun URL değerine yönlendirebilirsiniz. Go 1.27 ile gelen httptest.NewTestServer, bellek içi sahte ağ kullanır ve Client() ile verdiği istemci tüm HTTP ve HTTPS isteklerini test sunucusuna yönlendirir. Test yapısı için Go test yazma yazısına bakın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Go öğrenme rehberi · Go net/http web sunucusu · Go context paketi · Go encoding/json · Go hata yönetimi · Go select ve zaman aşımı