go
Go dersleri
Go/Ekosistem ve Pratik

HTTP Sunucusu ve İstemcisi

net/http ile REST API, yönlendirme desenleri, middleware ve düzgün kapanış.

Ders 21 / 2135 dkİleri
Bu derste öğreneceklerin
  • http.Handler ve http.HandlerFunc
  • http.ServeMux ile yöntem ve yol desenleri (GET /items/{id})
  • r.PathValue ve sorgu parametreleri
  • JSON API yazma
  • Middleware zinciri
  • http.Client ve zaman aşımları
  • Düzgün kapanış (graceful shutdown)
  • httptest ile handler testi

Go'nun en çok kullanıldığı alan web servisleridir ve bunun iyi bir sebebi vardır: net/http paketi, üretime hazır bir HTTP sunucusunu standart kütüphanenin içinde sunar. Başka bir dilde bir çerçeve (framework) seçip kurman, yapılandırman ve öğrenmen gereken şey, Go'da içeri alınan tek bir paketle gelir. Üstelik bu sunucu oyuncak değildir; dünyanın en yoğun trafikli sistemlerinin bir kısmı doğrudan onun üzerinde çalışır.

Go 1.22, yönlendirme (routing) konusunda uzun süredir beklenen bir gelişmeyi getirdi: http.ServeMux artık HTTP yöntemini ve yol değişkenlerini anlıyor. Önceden GET /items/{id} gibi bir deseni ifade etmek için üçüncü parti bir yönlendirici kullanmak neredeyse zorunluydu; artık standart kütüphane çoğu uygulama için yeterli. Bu ders bu yeni yeteneklerin üzerine kurulu.

Dersin sonunda http.Handler soyutlamasını, yöntem ve yol desenleriyle yönlendirmeyi, yol ve sorgu parametrelerini okumayı, JSON döndüren bir API yazmayı, middleware zinciri kurmayı, zaman aşımlı bir istemci yapılandırmayı, düzgün kapanışı (graceful shutdown) ve handler'larını ağ kullanmadan test etmeyi biliyor olacaksın.

http.Handler: her şeyin temeli

Go'da bir HTTP isteğini karşılayan her şey tek bir arayüzü uygular:

type Handler interface {
	ServeHTTP(w http.ResponseWriter, r *http.Request)
}

w, yanıtı yazdığın yerdir (bir io.Writer'dır); r ise isteğin tüm bilgisini taşır. Sıradan bir fonksiyonu handler'a çevirmek için http.HandlerFunc dönüşümü kullanılır — bu, bir fonksiyon tipine metot eklemenin en güzel örneklerinden biridir:

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r) // fonksiyonun kendisini çağırır
}

Yani http.HandlerFunc(myFunc) yazdığında, fonksiyonu ServeHTTP metoduna sahip bir değere dönüştürmüş olursun. Arayüzleri ve bu deseni Arayüzler dersinde ayrıntısıyla inceledik.

Handler'ları ağ olmadan çalıştırabilirsin: httptest.NewRecorder, bir sahte ResponseWriter verir ve yanıtı sana döndürür. Aşağıdaki program tam olarak bunu yapıyor — gerçek bir sunucu başlatmadan, handler'ı doğrudan çağırıyor:

main.go
package main

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

func hello(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("ad")
	if name == "" {
		name = "dünya"
	}
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, "Merhaba, %s!", name)
}

func main() {
	// Sahte bir istek ve yanıt kaydedici
	req := httptest.NewRequest(http.MethodGet, "/selam?ad=Zeynep", nil)
	rec := httptest.NewRecorder()

	hello(rec, req) // handler'ı doğrudan çağır

	res := rec.Result()
	fmt.Println("durum:", res.StatusCode)
	fmt.Println("tip:", res.Header.Get("Content-Type"))
	fmt.Println("gövde:", rec.Body.String())

	// Parametresiz istek
	rec2 := httptest.NewRecorder()
	hello(rec2, httptest.NewRequest(http.MethodGet, "/selam", nil))
	fmt.Println("varsayılan:", rec2.Body.String())
}
Çıktı
durum: 200
tip: text/plain; charset=utf-8
gövde: Merhaba, Zeynep!
varsayılan: Merhaba, dünya!

w.WriteHeader çağrısının sırası önemlidir: Başlıkları (w.Header().Set) mutlaka durum kodunu yazmadan önce ayarlamalısın. WriteHeader çağrıldıktan veya gövdeye ilk yazma yapıldıktan sonra eklenen başlıklar yok sayılır. Durum kodunu hiç yazmazsan Go, ilk gövde yazımında otomatik olarak 200 döndürür.

Yönlendirme: ServeMux desenleri

http.ServeMux, gelen istekleri kayıtlı desenlere göre handler'lara dağıtır. Go 1.22'den itibaren desen üç parçadan oluşabilir: [YÖNTEM ][ana_makine]/yol.

DesenEşleşir
/itemsHer yöntemle /items
GET /itemsYalnızca GET (ve HEAD) ile /items
GET /items/{id}/items/42id değişkeni
POST /items/{id}/tags/{tag}İki yol değişkeni
GET /files/{path...}Kalan tüm yolu yakalar
GET /static/Sonu / olan desen: alt yollara da uyar
GET /items/{$}Yalnızca tam olarak /items/
main.go
package main

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

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /urunler", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "tüm ürünler")
	})

	mux.HandleFunc("GET /urunler/{id}", func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id") // yol değişkeni
		fmt.Fprintf(w, "ürün %s", id)
	})

	mux.HandleFunc("POST /urunler", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusCreated)
		fmt.Fprint(w, "ürün oluşturuldu")
	})

	mux.HandleFunc("GET /dosyalar/{yol...}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "dosya yolu: %s", r.PathValue("yol"))
	})

	// Birkaç isteği sahte olarak çalıştır
	cases := []struct {
		method, target string
	}{
		{http.MethodGet, "/urunler"},
		{http.MethodGet, "/urunler/42"},
		{http.MethodPost, "/urunler"},
		{http.MethodDelete, "/urunler"}, // yöntem eşleşmez
		{http.MethodGet, "/dosyalar/a/b/c.txt"},
		{http.MethodGet, "/bilinmeyen"},
	}

	for _, c := range cases {
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, httptest.NewRequest(c.method, c.target, nil))
		fmt.Printf("%-6s %-22s%d %s\n", c.method, c.target, rec.Code, rec.Body.String())
	}
}
Çıktı
GET    /urunler               → 200 tüm ürünler
GET    /urunler/42            → 200 ürün 42
POST   /urunler               → 201 ürün oluşturuldu
DELETE /urunler               → 405 Method Not Allowed

GET    /dosyalar/a/b/c.txt    → 200 dosya yolu: a/b/c.txt
GET    /bilinmeyen            → 404 404 page not found

Yöntemi eşleşmeyen istek 405 Method Not Allowed, hiçbir desene uymayan istek 404 Not Found alır — bunları elle yazmana gerek yoktur. Desenler arasında çakışma olduğunda en özel olan kazanır: /urunler/{id} yerine /urunler/ozel varsa, /urunler/ozel isteği ikincisine gider.

JSON API yazmak

Gerçek bir API'de istekleri çözer, iş mantığını çalıştırır ve JSON döndürürsün. Aşağıdaki örnek, bellekte tutulan basit bir depo üzerinden tam bir CRUD akışı kurar:

main.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"sync"
)

type Task struct {
	ID    int    `json:"id"`
	Title string `json:"title"`
	Done  bool   `json:"done"`
}

type Store struct {
	mu     sync.Mutex
	nextID int
	items  map[int]Task
}

func NewStore() *Store { return &Store{nextID: 1, items: map[int]Task{}} }

func (s *Store) Add(title string) Task {
	s.mu.Lock()
	defer s.mu.Unlock()
	t := Task{ID: s.nextID, Title: title}
	s.items[t.ID] = t
	s.nextID++
	return t
}

func (s *Store) Get(id int) (Task, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	t, ok := s.items[id]
	return t, ok
}

// writeJSON, yanıtı tek noktadan biçimlendirir.
func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	if err := json.NewEncoder(w).Encode(v); err != nil {
		// Başlık zaten gönderildi; yalnızca günlüğe yazılabilir
		fmt.Println("yanıt yazılamadı:", err)
	}
}

func writeError(w http.ResponseWriter, status int, msg string) {
	writeJSON(w, status, map[string]string{"error": msg})
}

func routes(store *Store) http.Handler {
	mux := http.NewServeMux()

	mux.HandleFunc("POST /gorevler", func(w http.ResponseWriter, r *http.Request) {
		var body struct {
			Title string `json:"title"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			writeError(w, http.StatusBadRequest, "geçersiz JSON")
			return
		}
		if body.Title == "" {
			writeError(w, http.StatusUnprocessableEntity, "başlık boş olamaz")
			return
		}
		writeJSON(w, http.StatusCreated, store.Add(body.Title))
	})

	mux.HandleFunc("GET /gorevler/{id}", func(w http.ResponseWriter, r *http.Request) {
		id, err := strconv.Atoi(r.PathValue("id"))
		if err != nil {
			writeError(w, http.StatusBadRequest, "id sayı olmalı")
			return
		}
		t, ok := store.Get(id)
		if !ok {
			writeError(w, http.StatusNotFound, "görev bulunamadı")
			return
		}
		writeJSON(w, http.StatusOK, t)
	})

	return mux
}

func main() {
	handler := routes(NewStore())

	call := func(method, target, body string) {
		req := httptest.NewRequest(method, target, strings.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		rec := httptest.NewRecorder()
		handler.ServeHTTP(rec, req)
		fmt.Printf("%-5s %-16s %d %s", method, target, rec.Code, rec.Body.String())
	}

	call(http.MethodPost, "/gorevler", `{"title":"testleri yaz"}`)
	call(http.MethodPost, "/gorevler", `{"title":""}`)
	call(http.MethodPost, "/gorevler", `bozuk`)
	call(http.MethodGet, "/gorevler/1", "")
	call(http.MethodGet, "/gorevler/99", "")
	call(http.MethodGet, "/gorevler/abc", "")
}
Çıktı
POST  /gorevler        201 {"id":1,"title":"testleri yaz","done":false}
POST  /gorevler        422 {"error":"başlık boş olamaz"}
POST  /gorevler        400 {"error":"geçersiz JSON"}
GET   /gorevler/1      200 {"id":1,"title":"testleri yaz","done":false}
GET   /gorevler/99     404 {"error":"görev bulunamadı"}
GET   /gorevler/abc    400 {"error":"id sayı olmalı"}

Yanıt yazmayı tek bir yardımcıda toplamak (writeJSON, writeError) önemli bir alışkanlıktır: Başlık, durum kodu ve gövde biçimi her yerde aynı olur ve hata yanıtlarının şekli tek noktadan değiştirilebilir.

İstek gövdesini çözerken boyut sınırı koymak da iyi bir fikirdir: r.Body = http.MaxBytesReader(w, r.Body, 1<<20) satırı, kötü niyetli bir istemcinin gigabaytlarca veri göndererek belleği tüketmesini engeller.

Middleware zinciri

Middleware, bir handler'ı sarmalayıp ondan önce ve sonra iş yapan bir fonksiyondur. İmzası her zaman aynıdır: func(http.Handler) http.Handler. Günlük kaydı, kimlik doğrulama, sıkıştırma, panik kurtarma ve zaman aşımı tipik örneklerdir.

main.go
package main

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

// statusRecorder, yazılan durum kodunu yakalamak için ResponseWriter'ı sarar.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (s *statusRecorder) WriteHeader(code int) {
	s.status = code
	s.ResponseWriter.WriteHeader(code)
}

func Logger(log *[]string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
			next.ServeHTTP(rec, r)
			*log = append(*log, fmt.Sprintf("%s %s%d", r.Method, r.URL.Path, rec.status))
		})
	}
}

func Auth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") != "Bearer gizli" {
			http.Error(w, "yetkisiz", http.StatusUnauthorized)
			return // zinciri burada kes
		}
		next.ServeHTTP(w, r)
	})
}

func Recoverer(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				http.Error(w, "sunucu hatası", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// Chain, middleware'leri yazıldıkları sırayla uygular.
func Chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
	for i := len(middlewares) - 1; i >= 0; i-- {
		h = middlewares[i](h)
	}
	return h
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /gizli", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "gizli veri")
	})
	mux.HandleFunc("GET /patla", func(w http.ResponseWriter, r *http.Request) {
		panic("beklenmeyen durum")
	})

	var log []string
	handler := Chain(mux, Logger(&log), Recoverer, Auth)

	call := func(target, token string) {
		req := httptest.NewRequest(http.MethodGet, target, nil)
		if token != "" {
			req.Header.Set("Authorization", token)
		}
		rec := httptest.NewRecorder()
		handler.ServeHTTP(rec, req)
		fmt.Printf("%-8s %d %s\n", target, rec.Code, strings.TrimSpace(rec.Body.String()))
	}

	call("/gizli", "Bearer gizli")
	call("/gizli", "")
	call("/patla", "Bearer gizli")

	fmt.Println("günlük:")
	for _, line := range log {
		fmt.Println(" ", line)
	}
}
Çıktı
/gizli   200 gizli veri
/gizli   401 yetkisiz
/patla   500 sunucu hatası
günlük:
  GET /gizli → 200
  GET /gizli → 401
  GET /patla → 500

Sıralama önemlidir. Chain(mux, Logger, Recoverer, Auth) çağrısında istek önce Logger'a, sonra Recoverer'a, sonra Auth'a, en sonunda mux'a ulaşır. Yani günlük kaydı en dışta olduğu için yetkisiz istekleri de kaydeder; panik kurtarıcı Auth'tan önce geldiği için kimlik doğrulamada oluşan panikleri de yakalar.

statusRecorder deseni de dikkat çekicidir: http.ResponseWriter'ı gömerek onun tüm metotlarını devralır, yalnızca WriteHeader'ı gölgeleyerek araya girer. Gömme ve kompozisyonu Struct'lar ve Metotlar dersinde işledik.

Sunucuyu çalıştırmak ve düzgün kapanış

Üretimde http.ListenAndServe(":8080", mux) yeterli değildir: Varsayılan zaman aşımları yoktur ve süreç sonlandırıldığında devam eden istekler yarıda kesilir. Doğru kurulum şöyledir:

main.go
package main

import (
	"context"
	"errors"
	"log"
	"net/http"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /saglik", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second, // yavaş başlık saldırılarına karşı
		ReadTimeout:       15 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       60 * time.Second,
		MaxHeaderBytes:    1 << 20,
	}

	// SIGINT/SIGTERM geldiğinde iptal olan bir context
	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	go func() {
		log.Println("sunucu dinliyor:", srv.Addr)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Fatal("dinleme hatası:", err)
		}
	}()

	<-ctx.Done() // kapatma sinyali bekleniyor
	log.Println("kapanış başladı")

	// Devam eden isteklere en fazla 20 saniye tanı
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()

	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Println("düzgün kapanamadı:", err)
		_ = srv.Close()
	}
	log.Println("sunucu kapandı")
}

Shutdown, yeni bağlantı kabul etmeyi durdurur, boştaki bağlantıları kapatır ve devam eden isteklerin bitmesini bekler. Süre dolarsa hata döndürür; o noktada Close ile zorla kapatmak gerekir. Bu desen, dağıtım sırasında kullanıcıların yarıda kesilen istek görmesini engeller — Kubernetes gibi ortamlarda neredeyse zorunludur.

Zaman aşımlarını ayarlamak da isteğe bağlı değildir. Zaman aşımı olmayan bir sunucu, yavaş veya kötü niyetli istemcilerin bağlantıları sonsuza kadar açık tutmasına izin verir ve kaynakları tüketir.

İstemci tarafı

http.Get gibi paket düzeyindeki yardımcılar, zaman aşımı olmayan varsayılan istemciyi kullanır. Üretim kodunda her zaman kendi istemcini yapılandır:

client.go
client := &http.Client{
	Timeout: 10 * time.Second, // bağlantı + okuma + yönlendirmeler dâhil toplam
	Transport: &http.Transport{
		MaxIdleConns:        100,
		MaxIdleConnsPerHost: 10,
		IdleConnTimeout:     90 * time.Second,
	},
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
	return err
}
req.Header.Set("Accept", "application/json")

res, err := client.Do(req)
if err != nil {
	return fmt.Errorf("istek başarısız: %w", err)
}
defer res.Body.Close() // gövdeyi kapatmazsan bağlantı yeniden kullanılamaz

if res.StatusCode != http.StatusOK {
	return fmt.Errorf("beklenmeyen durum: %s", res.Status)
}

var data Payload
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
	return fmt.Errorf("yanıt çözülemedi: %w", err)
}

Üç kural: Yanıt gövdesini her zaman kapat (yoksa bağlantı havuzu sızar), isteği bir context ile oluştur (böylece iptal edilebilir), ve durum kodunu kontrol et — client.Do, 404 veya 500 yanıtları için hata döndürmez; yalnızca ağ düzeyinde bir sorun olduğunda hata verir.

Handler testi

Handler'ları test etmek için sunucu başlatmana gerek yoktur. httptest.NewRecorder ile handler'ı doğrudan çağırabilir, tam bir uçtan uca test istiyorsan httptest.NewServer ile gerçek bir sunucu ayağa kaldırabilirsin:

api_test.go
func TestGetTask(t *testing.T) {
	store := NewStore()
	created := store.Add("testleri yaz")
	handler := routes(store)

	tests := []struct {
		name       string
		target     string
		wantStatus int
	}{
		{"var olan", "/gorevler/1", http.StatusOK},
		{"olmayan", "/gorevler/99", http.StatusNotFound},
		{"geçersiz id", "/gorevler/abc", http.StatusBadRequest},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, tt.target, nil)
			rec := httptest.NewRecorder()

			handler.ServeHTTP(rec, req)

			if rec.Code != tt.wantStatus {
				t.Errorf("durum = %d; beklenen %d", rec.Code, tt.wantStatus)
			}
			if rec.Code == http.StatusOK {
				var got Task
				if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
					t.Fatalf("yanıt çözülemedi: %v", err)
				}
				if got.Title != created.Title {
					t.Errorf("başlık = %q; beklenen %q", got.Title, created.Title)
				}
			}
		})
	}
}

Bu testler ağ kullanmadığı için milisaniyeler içinde çalışır ve port çakışması gibi sorunlar yaşanmaz. Test yazmanın tüm ayrıntılarını Test ve Benchmark dersinde bulabilirsin.

Üretim için notlar

Çalışan bir servis yazmak ile güvenilir bir servis yazmak arasında birkaç alışkanlık farkı vardır.

Bağımlılıkları handler'a taşı. Veritabanı, önbellek ve yapılandırma gibi bağımlılıkları küresel değişkenlerde tutmak yerine bir struct'ta topla ve handler'ları o struct'ın metotları yap. Böylece testte sahte bir bağımlılık geçirmek kolaylaşır ve hangi handler'ın neye ihtiyaç duyduğu görünür olur.

Her isteğe bir kimlik ver. Gelen istekte bir izleme kimliği yoksa üret, context'e koy ve tüm günlük satırlarına ekle. Bir sorunu incelerken tek bir isteğin tüm yolculuğunu görebilmek, hata ayıklamayı bambaşka bir seviyeye taşır.

Hata yanıtlarını sızdırma. İç hata mesajlarını istemciye olduğu gibi döndürmek, veritabanı yapısı veya dosya yolları gibi bilgileri ifşa edebilir. İstemciye kısa ve genel bir mesaj ver, ayrıntıyı günlüğe yaz.

Girdiyi sınırla ve doğrula. Gövde boyutunu sınırla, beklenen alanları kontrol et, sayısal aralıkları doğrula. Doğrulamayı handler'ın en başında yapmak, iş mantığını temiz tutar.

Sağlık uçları ekle. Basit bir canlılık ucu ve bağımlılıkları kontrol eden bir hazırlık ucu, dağıtım araçlarının servisini doğru yönetmesini sağlar.

Zaman aşımlarını her katmanda ayarla. Sunucuda okuma/yazma, istemcide toplam süre, veritabanı çağrılarında context zaman aşımı. Zaman aşımı olmayan tek bir katman, tüm sistemin kilitlenmesine yetebilir.

Çerçeve gerekli mi?

Go ekosisteminde sık sorulan bir soru vardır: Standart kütüphane yeter mi, yoksa bir web çerçevesi mi kullanmalı? Cevap, projenin şekline ve ekibin alışkanlıklarına bağlıdır; ama son yıllarda terazi belirgin biçimde standart kütüphane tarafına kaydı.

Standart kütüphanenin lehine olan en güçlü argüman kararlılıktır. Go'nun uyumluluk sözü sayesinde, bugün yazdığın kod yıllar sonra da derlenir ve aynı biçimde çalışır. Üçüncü parti bir çerçeve ise sürüm atlayabilir, bakımsız kalabilir ya da yaklaşımını değiştirebilir; ve bir çerçeveye yaslanan uygulamayı taşımak nadiren kolaydır. Ayrıca bağımlılık sayısının az olması, güvenlik güncellemelerini takip etmeyi ve derleme sürelerini kısa tutmayı kolaylaştırır.

İkinci argüman öğrenilebilirliktir. Standart kütüphaneyle yazılmış bir servise bakan her Go geliştiricisi, ek bir şey öğrenmeden ne olduğunu anlar. Handler bir fonksiyondur, middleware bir sarmalayıcıdır, yönlendirme bir eşleme tablosudur. Çerçeveye özgü kavramlar, bağlam nesneleri ve sihirli davranışlar yoktur.

Çerçevelerin lehine olan argümanlar da gerçektir. Hazır gelen istek bağlama ve doğrulama, yapılandırılmış hata yönetimi, otomatik belge üretimi, oturum yönetimi ve şablon altyapısı, aynı işleri elle yazmaktan hızlıdır. Ekibin çok sayıda benzer uç nokta yazacaksa, tekrarlayan işi azaltan bir katman değerli olabilir.

Pratik bir orta yol şudur: Yönlendirme, middleware ve yanıt yazma gibi çekirdek işleri standart kütüphaneyle yap, tekrarlayan kalıpları kendi küçük yardımcı fonksiyonlarına topla. Böylece kodun anlaşılır kalır, bağımlılıkların az olur ve ihtiyaç duyduğun soyutlamaları kendi terimlerinle tanımlarsın. Gerçekten bir kütüphaneye ihtiyaç duyduğunda ise tek bir işi iyi yapan küçük paketleri tercih et; her şeyi kendi yöntemine göre yeniden tanımlayan büyük çerçeveler yerine.

Sık yapılan hatalar

  • Başlıkları WriteHeader'dan sonra ayarlamak. Sessizce yok sayılırlar.
  • Hata yazdıktan sonra return etmemek. Handler çalışmaya devam eder ve ikinci bir yanıt yazmaya çalışır.
  • Yanıt gövdesini kapatmamak (istemci tarafında). Bağlantı havuzu sızar.
  • client.Do'nun 500 için hata döneceğini sanmak. Durum kodunu sen kontrol etmelisin.
  • Zaman aşımsız sunucu ve istemci kullanmak. Yavaş istemciler kaynakları tüketir.
  • İstek gövdesine boyut sınırı koymamak. Bellek tüketimi saldırıya açık hâle gelir.
  • Middleware sırasını düşünmemek. Panik kurtarıcı en dışta değilse bazı panikleri yakalayamaz.
  • Handler'ları küresel duruma bağlamak. Test edilemez ve eşzamanlı erişimde güvensiz hâle gelirler.

Alıştırmalar

Alıştırma·Sağlık ve yankı uçları
Kolay

İki uç noktası olan bir mux kur: GET /saglik sabit bir JSON durum döndürsün, GET /yankı/{mesaj} ise yol değişkenini JSON içinde geri versin. Üç farklı istekle sahte olarak test et.

İpucu

r.PathValue("mesaj") yol değişkenini verir. Yanıtı json.NewEncoder(w).Encode(...) ile yaz.

Çözümü göster
main.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
)

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /saglik", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})

	mux.HandleFunc("GET /yanki/{mesaj}", func(w http.ResponseWriter, r *http.Request) {
		msg := r.PathValue("mesaj")
		writeJSON(w, http.StatusOK, map[string]any{"mesaj": msg, "uzunluk": len([]rune(msg))})
	})

	for _, target := range []string{"/saglik", "/yanki/merhaba", "/yanki/çağrı", "/yok"} {
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil))
		fmt.Printf("%-16s %d %s", target, rec.Code, rec.Body.String())
	}
}
Çıktı
/saglik          200 {"status":"ok"}
/yanki/merhaba   200 {"mesaj":"merhaba","uzunluk":7}
/yanki/çağrı     200 {"mesaj":"çağrı","uzunluk":5}
/yok             404 404 page not found

{mesaj} deseni yalnızca tek bir yol parçasını yakalar: /yanki/a/b isteği bu desene uymaz ve 404 alır. Birden çok parçayı yakalamak için {mesaj...} yazman gerekirdi.

Alıştırma·Doğrulayan POST ucu
Orta

Bir kullanıcı kaydı alan POST /kayit ucu yaz. Gövdede ad ve yas alanları olsun; ad boş olamaz, yaş 0–130 aralığında olmalı. Geçersiz JSON, eksik alan ve aralık dışı değer için farklı durum kodları döndür. Beş farklı istekle sına.

İpucu

Çözme hatası 400, doğrulama hatası 422 için uygundur. Yaşın "verilmedi" mi "sıfır" mı olduğunu ayırt etmek için *int kullan.

Çözümü göster
main.go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

type SignupRequest struct {
	Name string `json:"ad"`
	Age  *int   `json:"yas"`
}

func writeJSON(w http.ResponseWriter, status int, v any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func validate(req SignupRequest) []string {
	var problems []string
	if strings.TrimSpace(req.Name) == "" {
		problems = append(problems, "ad boş olamaz")
	}
	if req.Age == nil {
		problems = append(problems, "yas alanı zorunlu")
	} else if *req.Age < 0 || *req.Age > 130 {
		problems = append(problems, "yas 0-130 aralığında olmalı")
	}
	return problems
}

func signup(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB sınır

	var req SignupRequest
	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	if err := dec.Decode(&req); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "geçersiz JSON"})
		return
	}

	if problems := validate(req); len(problems) > 0 {
		writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"errors": problems})
		return
	}

	writeJSON(w, http.StatusCreated, map[string]any{"ad": req.Name, "yas": *req.Age})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /kayit", signup)

	bodies := []string{
		`{"ad":"Zeynep","yas":28}`,
		`{"ad":"","yas":28}`,
		`{"ad":"Ali"}`,
		`{"ad":"Can","yas":200}`,
		`{"ad":"Ece","yas":0}`,
		`bozuk json`,
		`{"ad":"Ece","yas":30,"fazla":true}`,
	}

	for _, body := range bodies {
		req := httptest.NewRequest(http.MethodPost, "/kayit", strings.NewReader(body))
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		fmt.Printf("%d%-42s %s", rec.Code, body, rec.Body.String())
	}
}
Çıktı
201 ← {"ad":"Zeynep","yas":28}                   {"ad":"Zeynep","yas":28}
422 ← {"ad":"","yas":28}                         {"errors":["ad boş olamaz"]}
422 ← {"ad":"Ali"}                               {"errors":["yas alanı zorunlu"]}
422 ← {"ad":"Can","yas":200}                     {"errors":["yas 0-130 aralığında olmalı"]}
201 ← {"ad":"Ece","yas":0}                       {"ad":"Ece","yas":0}
400 ← bozuk json                                 {"error":"geçersiz JSON"}
400 ← {"ad":"Ece","yas":30,"fazla":true}         {"error":"geçersiz JSON"}

Doğrulama sonucunu bir hata dilimi olarak döndürmek, istemciye tüm sorunları tek seferde bildirmeyi sağlar — ilk hatada durup sadece onu söylemek kullanıcıyı gereksiz yere birkaç tur attırır. DisallowUnknownFields, yazım hatalarını (örneğin yas yerine yaş) sessizce yok saymak yerine hata hâline getirir; bu, API sözleşmesini sıkı tutmanın kolay bir yoludur.

Alıştırma·Hız sınırlayıcı middleware
Zor

Her istemci için "en fazla N istek" kuralı uygulayan bir middleware yaz. İstemciyi bir başlıktaki anahtarla ayırt et, sayaçları eşzamanlı erişime karşı koru ve sınır aşıldığında 429 Too Many Requests döndür. Kalan hakkı bir yanıt başlığında bildir.

İpucu

Sayaçları map[string]int içinde tut ve sync.Mutex ile koru. Middleware imzası func(http.Handler) http.Handler olmalı.

Çözümü göster
main.go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
)

type RateLimiter struct {
	mu    sync.Mutex
	limit int
	used  map[string]int
}

func NewRateLimiter(limit int) *RateLimiter {
	return &RateLimiter{limit: limit, used: map[string]int{}}
}

// allow, isteği kabul edip etmediğini ve kalan hakkı döndürür.
func (rl *RateLimiter) allow(key string) (bool, int) {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	if rl.used[key] >= rl.limit {
		return false, 0
	}
	rl.used[key]++
	return true, rl.limit - rl.used[key]
}

func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		key := r.Header.Get("X-Api-Key")
		if key == "" {
			key = "anonim"
		}

		ok, remaining := rl.allow(key)
		w.Header().Set("X-RateLimit-Limit", fmt.Sprint(rl.limit))
		w.Header().Set("X-RateLimit-Remaining", fmt.Sprint(remaining))

		if !ok {
			http.Error(w, "istek sınırı aşıldı", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /veri", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "veri")
	})

	rl := NewRateLimiter(3)
	handler := rl.Middleware(mux)

	call := func(key string) {
		req := httptest.NewRequest(http.MethodGet, "/veri", nil)
		if key != "" {
			req.Header.Set("X-Api-Key", key)
		}
		rec := httptest.NewRecorder()
		handler.ServeHTTP(rec, req)
		fmt.Printf("%-8s %d kalan=%-2s %s\n",
			key, rec.Code, rec.Header().Get("X-RateLimit-Remaining"), strings.TrimSpace(rec.Body.String()))
	}

	for range 4 {
		call("ali")
	}
	call("ayse") // farklı anahtar: kendi kotası var
	call("ali")  // hâlâ engelli
}
Çıktı
ali      200 kalan=2  veri
ali      200 kalan=1  veri
ali      200 kalan=0  veri
ali      429 kalan=0  istek sınırı aşıldı
ayse     200 kalan=2  veri
ali      429 kalan=0  istek sınırı aşıldı

Sayaç haritası paylaşımlı bir kaynaktır ve birden çok goroutine aynı anda istek işleyebilir; bu yüzden mutex zorunludur. Gerçek bir hız sınırlayıcıda sayaçlar zamanla sıfırlanır — sabit pencere, kayan pencere ya da token bucket gibi algoritmalar kullanılır. Buradaki sürüm, deseni göstermek için kasıtla sadeleştirilmiştir.

Kalan hakkı başlıkta bildirmek yaygın bir uygulamadır: İstemci kotasını görür ve isteklerini kendisi düzenleyebilir. Eşzamanlı erişimi korumanın yollarını Senkronizasyon dersinde ayrıntısıyla inceledik.

Kısa sınav

Kısa sınav

Go 1.22 ile birlikte http.ServeMux hangi yeteneği kazandı?

w.Header().Set(...) çağrısı ne zaman yapılmalıdır?

Bir middleware fonksiyonunun standart imzası nedir?

client.Do çağrısı 500 durum kodlu bir yanıt aldığında ne yapar?

srv.Shutdown(ctx) ne yapar?

Handler testinde httptest.NewRecorder ne sağlar?

Özet

  • http.Handler tek metotlu bir arayüzdür; http.HandlerFunc sıradan fonksiyonları handler'a çevirir.
  • Go 1.22'den beri ServeMux desenleri yöntem ve yol değişkeni içerebilir; değişkenler r.PathValue ile okunur.
  • Eşleşmeyen yöntem 405, eşleşmeyen yol 404 üretir; belirsiz desenler kayıt sırasında panik oluşturur.
  • Başlıklar WriteHeader'dan önce ayarlanmalıdır; hata yazdıktan sonra mutlaka return et.
  • JSON yanıtlarını tek bir yardımcıda topla; istek gövdesine boyut sınırı koy.
  • Middleware imzası func(http.Handler) http.Handler'dır; sıralama davranışı belirler.
  • Üretimde http.Server'ı zaman aşımlarıyla yapılandır ve sinyal geldiğinde Shutdown ile düzgün kapan.
  • İstemcide zaman aşımı ayarla, gövdeyi kapat ve durum kodunu kendin kontrol et.
  • httptest.NewRecorder ile handler'ları ağ kullanmadan, hızlı ve deterministik biçimde test et.
Bu dersi bitirdin mi?
İlerlemen bu tarayıcıda saklanır.