"go func()" Yazmak Kolay, Yönetmek Zor
Go dilini sevmemin en büyük sebebi concurrency modelidir. Bir goroutine başlatmak için sadece go keyword'ü yeterli. Thread oluşturmak için sayfalarca kod yazmak yok, callback cehennemine düşmek yok.
Ama bu kolaylık, bir tuzak da barındırıyor. Birçok projede gördüğüm manzara şu: Her yere go func() serpiştirilmiş, goroutine'ler kontrol edilemiyor, hatalar sessizce yutulmuş ve servis rastgele zamanlarda patlıyor.
Bu yazıda Go'nun concurrency modelini gerçek production senaryolarıyla anlatacağım. Teoriyi zaten biliyorsunuzdur; burada onu nasıl doğru kullanacağınızı göreceğiz.
Goroutine Nedir? (Kısa Hatırlatma)
Goroutine, Go runtime tarafından yönetilen hafif bir thread'dir. Bir OS thread'i megabyte'larca RAM tüketirken, bir goroutine sadece birkaç kilobyte ile başlar. Bu sayede binlerce, hatta yüz binlerce goroutine'i aynı anda çalıştırabilirsiniz.
func main() {
go doSomething() // Bu kadar. Yeni bir goroutine başladı.
time.Sleep(time.Second) // Ana goroutine'in bitmesini bekle (kötü pratik, aşağıda düzelteceğiz)
}Sorun şu: go keyword'ü fire-and-forget mantığıyla çalışır. Goroutine'in ne zaman biteceğini, hata verip vermediğini bilmezsiniz. İşte production'da sorunlar tam burada başlıyor.
Hata 1: Goroutine'leri Beklemeden Çıkmak
En klasik hata. main fonksiyonu bittiğinde tüm goroutine'ler anında öldürülür.
func main() {
go processOrder(123) // Sipariş işleniyor...
// main bitti, goroutine yarı yolda öldü. Sipariş kayboldu.
}Çözüm: sync.WaitGroup
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
processOrder(123)
}()
wg.Wait() // Tüm goroutine'ler bitene kadar bekle
}WaitGroup basit ama etkilidir. Kaç goroutine başlattığınızı sayar ve hepsi Done() çağırana kadar bekler.
Hata 2: Hataları Sessizce Yutmak
Goroutine içinde panic olursa ne olur? Sadece o goroutine ölür, ana program devam eder. Gece 3'te alarm gelmeden servisiniz yarı ölü çalışıyor olabilir.
go func() {
result, err := fetchData()
if err != nil {
return // Hata yutuldu, kimse bilmiyor!
}
// ...
}()Çözüm: Error Channel
errChan := make(chan error, 1)
go func() {
result, err := fetchData()
if err != nil {
errChan <- err
return
}
// İşlem başarılı
errChan <- nil
}()
if err := <-errChan; err != nil {
log.Printf("Goroutine hatası: %v", err)
}Daha büyük sistemlerde golang.org/x/sync/errgroup paketini kullanın. Birden fazla goroutine'i yönetir ve ilk hatada hepsini durdurabilir.
Hata 3: Goroutine Leak (Sızıntısı)
Bir goroutine sonsuza dek bir channel'dan okuma bekliyorsa ve o channel'a hiç veri gelmiyorsa, o goroutine asla ölmez. Bellek sızıntısı başlar.
func fetch(url string) {
resultChan := make(chan string)
go func() {
// HTTP isteği 30 saniye timeout alırsa...
resp := httpGet(url)
resultChan <- resp // Buraya asla ulaşamaz
}()
select {
case result := <-resultChan:
fmt.Println(result)
case <-time.After(5 * time.Second):
fmt.Println("Timeout!")
// Goroutine hala yaşıyor ve channel'a yazmayı bekliyor. LEAK!
}
}Çözüm: context.Context ile İptal
func fetch(ctx context.Context, url string) {
resultChan := make(chan string, 1) // Buffered channel!
go func() {
resp := httpGet(url)
select {
case resultChan <- resp:
case <-ctx.Done():
return // Context iptal edildi, temiz çık
}
}()
select {
case result := <-resultChan:
fmt.Println(result)
case <-ctx.Done():
fmt.Println("İptal edildi")
}
}context.Context Go'nun en önemli parçalarından biridir. Her HTTP handler'ı, her database sorgusu bir context almalıdır. Böylece üst katmandan "dur" komutu gönderebilirsiniz.
Pattern: Worker Pool
Diyelim ki 10.000 tane URL'i crawl edeceksiniz. Her biri için bir goroutine başlatmak sistemi boğar. Çözüm: Sabit sayıda worker, sınırsız iş kuyruğu.
func main() {
jobs := make(chan string, 100)
results := make(chan string, 100)
// 10 tane worker başlat
for i := 0; i < 10; i++ {
go worker(jobs, results)
}
// İşleri kuyruğa ekle
go func() {
for _, url := range urls {
jobs <- url
}
close(jobs) // Tüm işler eklendi
}()
// Sonuçları topla
for range urls {
result := <-results
fmt.Println(result)
}
}
func worker(jobs <-chan string, results chan<- string) {
for url := range jobs {
resp := fetch(url)
results <- resp
}
}Bu pattern CPU veya I/O bound işler için altın standarttır. Worker sayısını runtime.NumCPU() ile dinamik de yapabilirsiniz.
Gerçek Hayat: Rate Limiter
Bir API'ye saniyede en fazla 10 istek atmanız gerekiyor. Goroutine'leri serbest bırakırsanız API sizi banlar.
func main() {
limiter := time.NewTicker(100 * time.Millisecond) // Saniyede 10
defer limiter.Stop()
for _, url := range urls {
<-limiter.C // Bir tick bekle
go fetch(url)
}
}Daha sofistike çözümler için golang.org/x/time/rate paketine bakın.
Sonuç: Concurrency Kolay, Correctness Zor
Go'da goroutine başlatmak bir satır. Ama doğru, güvenli ve ölçeklenebilir concurrent kod yazmak disiplin ister.
Checklist:
- [ ] Her goroutine'in sonlanma garantisi var mı? (
WaitGroup,errgroup) - [ ] Hatalar düzgün raporlanıyor mu? (Error channel veya errgroup)
- [ ] Context kullanarak iptal mekanizması sağlandı mı?
- [ ] Goroutine leak potansiyeli analiz edildi mi? (pprof ile kontrol)
- [ ] Worker pool gibi sınırlayıcı patternler uygulandı mı?
Concurrency, Go'nun en güçlü yanı. Ama bu gücü kontrol altında tutmak sizin işiniz. Eğer mevcut Go projenizde garip davranışlar, bellek sızıntıları veya race condition'lar varsa, dışarıdan bir göz işe yarayabilir.
