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:
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())
}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.
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())
}
}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:
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", "")
}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.
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)
}
}/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:
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 := &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:
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
returnetmemek. 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
İ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
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())
}
}/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.
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
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())
}
}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.
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
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
}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
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.Handlertek metotlu bir arayüzdür;http.HandlerFuncsıradan fonksiyonları handler'a çevirir.- Go 1.22'den beri
ServeMuxdesenleri yöntem ve yol değişkeni içerebilir; değişkenlerr.PathValueile 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 mutlakareturnet. - 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ğindeShutdownile düzgün kapan. - İstemcide zaman aşımı ayarla, gövdeyi kapat ve durum kodunu kendin kontrol et.
httptest.NewRecorderile handler'ları ağ kullanmadan, hızlı ve deterministik biçimde test et.