Go'da HTTP servisi yazmak: standart kütüphane yeter mi
Go'nun net/http paketiyle küçük bir HTTP servisi kurarken neyin geldiğini, neyin gelmediğini ve framework eşiğini tartışıyorum.
Go öğrenirken kendime verdiğim kurallardan biri şuydu: bir şeyi standart kütüphane ile kurmayı denemedikçe dışarıdan kütüphane eklemeyeceğim. Bu kural bazen can sıktı, çoğu zaman öğretti — Go öğrenmeye neden başladığımı anlattığım yazıdaki niyetin doğrudan sonucuydu: dili araçlarının arkasından değil, kendisinden tanımak. HTTP servisi bu kuralı gerçekten test eden alan oldu.
PHP’de her şeyin üstünde bir framework çalışıyor; Laravel, Slim, ne olursa. Ham PHP ile HTTP sunucusu yazmak teorik olarak mümkün ama kimse yapmıyor. Go’da ise net/http paketi gerçekten kullanılabilir, production’da çalışan servisler standart kütüphaneyle yazılıyor. Bu fark zihniyet değişikliğine zorluyor.
net/http ile temel sunucu
Bir Go HTTP sunucusunu çalıştırmak birkaç satır:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok"}`)
})
http.ListenAndServe(":8080", nil)
}
Çalıştırıyor, port 8080’de yanıt veriyor. Bağımlılık sıfır. Bu basitlik ilk başta tatmin edici; ama bir API yazmaya başladığınızda sınırlar ortaya çıkıyor.
Standart kütüphanenin eksikleri
net/http’nin HandleFunc ile kayıt mekanizması yeterince esnek değil. Route parametresi — /users/123 gibi dinamik segmentler — bu yazıyı yazdığım sırada standart kütüphanede yoktu. Her yolu elle parse etmek gerekiyordu:
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
// "/users/" sonrasını elle çekiyoruz
id := r.URL.Path[len("/users/"):]
if id == "" {
// liste döndür
return
}
// tekil kayıt döndür
})
Bu yaklaşım iki-üç rota için idare ediyor, on rota için çekilmez hale geliyor. HTTP metodu (GET/POST/PUT/DELETE) ayrımını da elle yapmak gerekiyor:
switch r.Method {
case http.MethodGet:
// GET işle
case http.MethodPost:
// POST işle
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
Bu switch bloğunu her handler’a kopyalamak kodu hızla şişiriyor. Standart kütüphane bu ortak pattern’ı soyutlamıyor; bunu kendiniz yazmak ya da bir kütüphaneye devretmek gerekiyor.
Buraya bugünden bir not düşmek gerekiyor: bu eksik kapandı. Go 1.22, net/http’nin ServeMux’ına gelişmiş desen eşleştirmesi ekledi — /items/{id} gibi joker segmentler artık standart kütüphanede tanınıyor ve segmentin değerine Request.PathValue ile ulaşılıyor; "POST /items/create" biçiminde kayıt yapıldığında handler yalnız o metotla çağrılıyor. Yani yukarıdaki elle parse ve switch r.Method kalıpları 2024’ten beri zorunlu değil. Yazının geri kalanındaki muhakeme — bir kütüphaneyi ne zaman almalı — duruyor; değişen, eşiğin yeri.
Framework eşiği nerede?
Bir kütüphane almadan önce şu soruyu soruyorum: standart kütüphanenin sağlamadığı şey ne kadar jenerik bir ihtiyaç? Route parametresi ve HTTP metodu yönlendirmesi jenerik ve evrensel; her serviste gerekli.
Bu noktada üçüncü parti kütüphanelere bakıyorum. Go ekosisteminde gorilla/mux ve chi bu boşluğu dolduran hafif router’lar. Framework değiller; yalnızca net/http’nin üstüne route parametresi ve metot eşleştirme ekliyorlar, standart http.Handler interface’ine uyuyorlar.
import "github.com/go-chi/chi"
r := chi.NewRouter()
r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) {
id := chi.URLParam(req, "id")
// id ile işle
})
http.ListenAndServe(":8080", r)
Temel felsefe korunuyor: standart kütüphanenin interface’lerine uyan, başka kütüphanelerle çakışmayan bir eklenti. Laravel benzeri büyük bir bağımlılık yığını değil.
chi’nin beni ikna eden tarafı standart http.Handler interface’ine tam uyum — kendi tanıtımında “net/http ile %100 uyumlu” ve “dış bağımlılık yok, yalnız Go standart kütüphanesi” diyor. Bir kütüphane seçerken bu uyuma dikkat ediyorum; standart interface’i bozan bir router, ekosisteme uymayan middleware ya da yardımcılarla çalışmayı zorlaştırıyor. chi ile yazdığım handler’ı başka herhangi bir net/http uyumlu yapıyla kullanmak mümkün.
JSON yanıtları ve hata yönetimi
Standart kütüphanede JSON serialization için encoding/json paketi var ve iyi çalışıyor:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
Bu küçük helper fonksiyon tekrar yazmayı ortadan kaldırıyor; tüm handler’lar bunu kullanıyor.
PHP ile karşılaştırma
Go’da HTTP servisi yazmak PHP’de framework olmadan yazmaktan farklı: standart kütüphane gerçekten production’a hazır, bakımlı ve hızlı. Ancak Laravel’in sunduğu kimlik doğrulama, ORM, queue, e-posta gibi katmanlar Go’da yok — ve olması gerekmiyor. Go servisleri çoğunlukla daha dar bir sorumluluğu çok iyi yapıyor; geniş kapsam için başka diller devreye giriyor.
Sonuç olarak: Go’da standart kütüphane çok fazlasını karşılıyor, ama her şeyi değil. Route parametresi ve metot yönlendirmesi için hafif bir kütüphane makul bir ödünleşim. Tam framework’e geçmek çoğu durum için erken bir karar — ihtiyaç gerçekten büyüdüğünde alınabilir bir karar.
Kaynaklar
Bu yazının arkasındaki araştırmalar
PHP ile Go'yu aynı OAuth2 API'de ölçtüm: 10.000 yazmada fark yok, 50.000 okumada var
Her istekte OAuth2 token'ı doğrulayıp PostgreSQL'e yazan ya da okuyan aynı API, dört çekirdekte PHP-FPM, FrankenPHP worker ve Go ile saniyede 10.000 yazmayı ve 50.000 okumayı kaç CPU'yla karşılıyor?
Bulgu
Saniyede 10.000 yazmada üç aday da hedefi beş tekrarın beşinde tutturdu, p99 üçünde de 2,5 ms'yi aşmadı: bu yükte dil bir kapasite kalemi değil. Saniyede 50.000 okumayı dört çekirdekte yalnız Go tutturdu (p99 6,45 ms); PHP-FPM 23.528'de, FrankenPHP 22.859'da kaldı. İstek başına CPU okumada Go'da 64, FrankenPHP'de 117, PHP-FPM'de 168 mikrosaniye. 50.000 okuma için instance hesabı Go'ya 3,4, iki PHP adayına 8,2–8,8 çekirdek yazdırıyor. FrankenPHP'nin CPU tasarrufu kapasiteye dönmüyor: doygunken dört çekirdeğin birini boşta bırakıyor.
bugün ölçüldü
Bu konuda sorulanlar
Go HTTP sunucusunda panic'i recovery middleware ile nasıl yönetirim?
Zincirin en dışına `defer recover()` koyun, stack'i request id ile loglayıp generic 500 dönün, metrik basın, her goroutine'e kendi recover'ını verin.
Mikroservislerde dağıtık izlemeyi (OpenTelemetry) nasıl kurarım?
W3C Trace Context'te standartlaşıp propagation'ı OTel SDK'ya bırakın ve traceparent'ı kuyruk mesajının header'ına koyun; zincir orada kopuyor.
Bu konudaki deneyler
Dile özgü serileştirme yerine dondurulmuş bir JSON zarfı; polyglot kuyruk standardı BabelQueue'ya dönüştü.
Şu an ne yapıyor
Dondurulmuş JSON zarfı dört dilde — PHP, Python, Go, Node.js — aynı baytları okutuyor; sidecar ya da broker eklentisi gerekmiyor. Polyglot kuyruk kuran ekipler spesifikasyonu ve SDK'ları bugün kullanabilir.
Diff'i makineden çıkarmadan bir modele denetleten, sağlayıcıdan bağımsız Go CLI; CommitBrief ürününe dönüştü.
Şu an ne yapıyor
Diff hiç makineden çıkmadan yerel kod incelemesi yapıyor; `Provider` arayüzü sayesinde Anthropic, OpenAI, Gemini ve Ollama aynı sözleşmeyle takılıyor. İmzalı binary'ler yayında — diff'ini bir bulut sağlayıcısına vermek istemeyen herkes kurabilir.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.