# Birbirine bağımlı birden fazla işi, ilk hata hepsini iptal edecek şekilde errgroup ile mi yürütmeliyim?

> `errgroup.WithContext`'e geçip üstüne request-level bir timeout koyun ve ctx'i her HTTP/DB çağrısına geçirin; ihmal edilen ctx, iptali sessizce öldürür.

- Soruldu: 2026-08-30
- Yanıtlandı: 2026-09-04
- Soran: Onur
- Etiketler: go, concurrency
- Kaynak: https://www.muhammetsafak.com.tr/sor-bakalim/birbirine-bagimli-birden-fazla-isi-ilk-hata-hepsini-iptal-edecek-sekilde/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
**Soru:** Go ile yazdığımız bir API'de her isteğin başında beş ayrı upstream servise (kullanıcı, abonelik, kampanya, envanter ve fiyatlandırma) paralel HTTP çağrısı yapıyoruz. Şu an bunları düz bir `sync.WaitGroup` ile fan out edip tüm sonuçları topluyorum.

Sorun şu: bu beş çağrıdan biri hata verdiği anda geri kalan dördünün cevabını beklemenin anlamı kalmıyor, çünkü isteği zaten hatayla bitireceğim. İlk hata anında tüm batch'i iptal etmek için `errgroup`'a mı geçmeliyim, yoksa bu overkill mi olur?


Kısa cevap: Evet, geçin.

## Kısa cevap

Bu tam olarak `errgroup.WithContext`'in çözmek için var olduğu senaryo; burada overkill değil, doğru araç. `sync.WaitGroup` size "hepsini bekle" verir, sizin istediğiniz ise "biri düşerse hepsini bırak". errgroup, `golang.org/x/sync/errgroup` altında yaşayan küçük ve olgun bir bağımlılıktır; bu tür fan-out'lar için taşımaya kesinlikle değer.

## Neden

1. **errgroup.WithContext deyimi budur.** `WithContext`, gruba bir türetilmiş context verir ve goroutine'lerden biri `nil` olmayan bir hata döndürdüğü anda bu context'i iptal eder. Yani ilk hata, kalan çağrılara "durun" sinyalini otomatik gönderir; sizin [elle sinyalleşme kurmanıza](/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) gerek kalmaz.

2. **İptal işbirlikçidir — çağrılarınız ctx'e uymak zorunda.** Burası en sık atlanan nokta. `ctx` iptal olsa bile, HTTP çağrınız `http.NewRequestWithContext` ile bu context'i taşımıyorsa iptalin hiçbir etkisi olmaz; çağrı bitene kadar çalışmaya devam eder. Aynısı DB sorguları için de geçerli. Context'i tabana kadar geçirmezseniz errgroup'un yarısı boşa gider.

   ```go
   g, ctx := errgroup.WithContext(ctx)
   results := make([]Result, len(ids))
   for i, id := range ids {
       i, id := i, id // Go 1.22 öncesi döngü değişkeni yakalama
       g.Go(func() error {
           r, err := lookup(ctx, id) // ctx içeride http.NewRequestWithContext'e gider
           if err != nil {
               return fmt.Errorf("lookup %s: %w", id, err)
           }
           results[i] = r
           return nil
       })
   }
   if err := g.Wait(); err != nil {
       return nil, err // yalnızca İLK hata
   }
   ```

3. **g.Wait() yalnızca ilk hatayı döner.** Diğer goroutine'lerin hataları düşürülür. Beş çağrının hepsinin hatasını toplamak istiyorsanız errgroup yanlış araçtır; o zaman `WaitGroup` + kanal ya da `errors.Join` gerekir. Ama sizin ihtiyacınız "ilki yeter, gerisini iptal et" olduğu için birebir uyuyor.

4. **Wait() yine de tüm goroutine'ler dönene kadar bloklar.** İptal "durun" der ama `Wait`, her goroutine gerçekten geri dönene kadar beklemeyi sürdürür. Ctx'i yok sayan bir çağrınız varsa yine en yavaşı kadar beklersiniz — bu yüzden 2. madde kritik.

## Ne yapmalı

1. **Paylaşılan sonuçlara senkronizasyonsuz yazmayın.** Her goroutine kendi `results[i]` indeksine yazıyorsa güvenlidir (ayrı bellek hücreleri). Ama ortak bir `map`'e yazarsanız data race olur — `-race` ile yakalanır. Slice indeksi kullanın ya da mutex koyun.

2. **Fan out büyürse SetLimit.** Beş çağrı için sınırsız concurrency sorun değil, ama bu sayı büyürse `g.SetLimit(n)` ile eşzamanlı goroutine sayısını kapatın; upstream'i ezmeyin.

3. **Üst context'e bir deadline koyun.** İptal, yalnızca çağrılardan biri hata dönerse tetiklenir. Hiçbiri hata dönmüyor ama biri asılı kalıyorsa (yavaş upstream, yanıt vermeyen soket), `errgroup` sonsuza kadar bekler. Bunun için `WithContext`'e verdiğiniz context'i `context.WithTimeout` ile sarın; deadline dolduğunda ctx iptal olur, ctx'e uyan tüm çağrılar geri döner ve isteğiniz belirli bir üst sınırda biter.

**Sonuç:** Ben olsam `errgroup.WithContext`'e geçer, üstüne bir request-level timeout koyar, ctx'i her HTTP/DB çağrısına kadar geçirir ve sonuçları per-index slice'a yazardım. Go 1.22+ kullanıyorsanız döngü değişkeni yakalama satırına da gerek yok. Kısacası: hem "biri düşerse bırak" hem de "kimse dönmezse zamanaşımına uğra" garantisini birlikte kurun.

## İlgili Yazılar

- [Go'da eşzamanlılık: goroutine ve channel pratiği](/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/) — Blog
- [İç içe geçmiş servis çağrılarında context iptalini doğru şekilde nasıl yayarım?](https://www.muhammetsafak.com.tr/sor-bakalim/ic-ice-gecmis-servis-cagrilarinda-context-iptalini-dogru-sekilde-nasil-yayarim/) — Sor Bakalım
- [Goroutine sızıntılarını uzun süre çalışan bir Go servisinde nasıl tespit edip önlerim?](https://www.muhammetsafak.com.tr/sor-bakalim/goroutine-sizintilarini-uzun-sure-calisan-bir-go-servisinde-nasil-tespit-edip/) — Sor Bakalım
- [Go CLI aracını GoReleaser ve Homebrew ile çoklu platforma nasıl dağıtırım?](https://www.muhammetsafak.com.tr/sor-bakalim/go-cli-aracini-goreleaser-ve-homebrew-ile-dagitmak/) — Sor Bakalım
