Go'da Concurrency Pattern'leri: context, sync, errgroup, Worker Pool ve Goroutine Leak
Go'da eşzamanlı erişilen veri ya bir mutex ile korunur ya da bir channel ile sahibine bırakılır; korunmayan map process'i çökertir, korunmayan sayaç ise sessizce aynı sipariş ID'sini üretir. Bookstore'da concurrent map writes ve race detector, Service ile memstore'un mutex ile düzeltilmesi, context ile iptal ve deadline, WaitGroup.Go, errgroup ile 10.000 faturalık worker pool, Go 1.27'nin goroutineleak profili ve synctest ile 15 dakikalık testi anında çalıştırmak.
Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; module'ün go satırı 1.26.0'dır ve kod Go 1.26.11 ile de derlenir. golang.org/x/sync v0.25.0 kullanıldı. WaitGroup.Go ve testing/synctest Go 1.25 ile gelmiştir; goroutineleak profili Go 1.27'de genel kullanıma açılmıştır ve onu gösteren lab //go:build go1.27 ile ayrılmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) 04'ten beri, C# tarafındaki Bookstore.Ordering module'ünün karşılığı olan bir orders.Service ve bellekte çalışan bir memstore var. İkisinin yorumunda da "eşzamanlı kullanım için güvenli değil" yazıyordu. Şimdi 100 müşteri aynı anda Dune sipariş ediyor:
// Lab 08: 100 müşteri aynı anda sipariş veriyor.
package main
import (
"context"
"fmt"
"sync"
"bookstore/internal/catalog"
"bookstore/internal/orders"
"bookstore/internal/orders/memstore"
)
func main() {
ctx := context.Background()
repo := memstore.New()
svc := orders.NewService(repo, catalog.Store{})
const n = 100
ids := make([]string, n)
var wg sync.WaitGroup
for i := range n {
wg.Go(func() { // Go 1.25: Add(1), go ve defer Done() tek çağrıda
o, err := svc.Place(ctx, fmt.Sprintf("customer-%d", i),
[]orders.Line{{ISBN: "9780441172719", Quantity: 1}})
if err != nil {
fmt.Println("place:", err)
return
}
ids[i] = o.ID // her goroutine sadece kendi index'ine yazar
})
}
wg.Wait()
unique := map[string]bool{}
for _, id := range ids {
unique[id] = true
}
fmt.Printf("orders placed: %d, unique IDs: %d\n", n, len(unique))
}
Program 100 sipariş ve 100 farklı ID mi yazar, yoksa daha azını mı?
go run ./labs/08-orders
fatal error: concurrent map writes
goroutine 13 [running]:
internal/runtime/maps.fatal({0x10227bb1e?, 0x102247b0c?})
/usr/local/go/src/runtime/panic.go:1197 +0x20
bookstore/internal/orders/memstore.(*Store).Save(0x0?, {0x6?, 0x1d0c56084e98?}, 0x1d0c560880e0)
/home/user/bookstore/internal/orders/memstore/memstore.go:26 +0x30
bookstore/internal/orders.(*Service).Place(...)
/home/user/bookstore/internal/orders/service.go:67 +0x5a4
main.main.func1()
/home/user/bookstore/labs/08-orders/main.go:24 +0xa8
...
exit status 2
Hiçbir şey yazmadı; beş denemenin beşinde de process çöktü. Go'nun map'i eşzamanlı yazmaya karşı korunmuyor ve runtime bunu fark ettiğinde recover edilemeyen bir fatal error ile programı durduruyor. Bu, şanslı olduğumuz durum: hata yüksek sesle geldi. Map'i düzelttikten sonra çok daha sessiz bir hata kalacak. Bu yazıda sync package'ını, race detector'ı, context.Context'i, errgroup ile worker pool'u ve goroutine leak tespitini, Bookstore'un sipariş servisini eşzamanlı kullanıma uygun hâle getirerek göreceğiz.
sync.Mutex ve RWMutex: memstore'u Düzeltmek
Bir map'e birden fazla goroutine erişecekse erişimi bir mutex ile sırayla yaptırırsın. memstore'a bir sync.RWMutex ekledim: yazma için Lock, okuma için RLock. RWMutex'te aynı anda birçok okuyucu girebilir, yazıcı ise tek başına girer. Zero value'su kullanıma hazır; ayrıca başlatmak gerekmiyor.
// Store, eşzamanlı kullanıma uygun, bellekte çalışan bir sipariş deposu.
type Store struct {
mu sync.RWMutex // byID'yi korur
byID map[string]*orders.Order
}
// *Store'un orders.Repository'yi sağladığını derleme zamanında kontrol eder.
var _ orders.Repository = (*Store)(nil)
// New boş bir store döndürür.
func New() *Store {
return &Store{byID: make(map[string]*orders.Order)}
}
// Save siparişi saklar; aynı ID'li sipariş varsa onun yerine yazar.
func (s *Store) Save(_ context.Context, o *orders.Order) error {
s.mu.Lock()
defer s.mu.Unlock()
s.byID[o.ID] = o
return nil
}
// ByID verilen ID'deki siparişi döndürür.
func (s *Store) ByID(_ context.Context, id string) (*orders.Order, error) {
s.mu.RLock()
defer s.mu.RUnlock()
o, ok := s.byID[id]
if !ok {
return nil, fmt.Errorf("id %s: %w", id, orders.ErrNotFound)
}
return o, nil
}
Mutex'i koruduğu alanın hemen üstüne koyup yorumda neyi koruduğunu yazmak Go'da yerleşik bir alışkanlık. defer s.mu.Unlock() ile kilit, fonksiyondan hangi yoldan çıkılırsa çıkılsın açılıyor. C# Asenkron Programlama serisinin "C# Senkronizasyon" yazısındaki lock ifadesinin karşılığı bu; farkı, Go'da kilidin bir değer olması ve lock gibi bir syntax olmaması.
go run ./labs/08-orders
orders placed: 100, unique IDs: 94
orders placed: 100, unique IDs: 95
orders placed: 100, unique IDs: 93
orders placed: 100, unique IDs: 96
Programı dört kez çalıştırdım. Çökme gitti ama 100 siparişin 4 ile 7 tanesi başka bir siparişle aynı ID'yi aldı. Aynı ID ile kaydedilen sipariş öncekinin üzerine yazdığı için bu siparişler sessizce kayboldu. Hiçbir hata, hiçbir log yok.
Race Detector: Sessiz Hatayı Bulmak
Go'nun race detector'ı derleyiciye eklenen bir araç: -race ile derlenen program her bellek erişimini izliyor ve iki goroutine'in aynı adrese senkronizasyon olmadan eriştiğini, en az birinin yazdığını gördüğünde rapor veriyor.
go run -race ./labs/08-orders
==================
WARNING: DATA RACE
Read at 0x00c0000a2148 by goroutine 9:
bookstore/internal/orders.(*Service).Place()
/home/user/bookstore/internal/orders/service.go:61 +0x4a8
main.main.func1()
/home/user/bookstore/labs/08-orders/main.go:24 +0xe4
Previous write at 0x00c0000a2148 by goroutine 8:
bookstore/internal/orders.(*Service).Place()
/home/user/bookstore/internal/orders/service.go:61 +0x4bc
main.main.func1()
/home/user/bookstore/labs/08-orders/main.go:24 +0xe4
...
==================
orders placed: 100, unique IDs: 57
Found 3 data race(s)
exit status 66
service.go:61, 05'ten kalan s.nextID++ satırı. Bu işlem tek bir adım değil: değeri oku, bir artır, geri yaz. İki goroutine aynı değeri okuyup ikisi de 1001 yazınca iki sipariş aynı ID'yi alıyor. Concurrency Teorisi serisinin "Race Condition ve Data Race: Check-Then-Act, Read-Modify-Write ve Atomicity" yazısında anlatılan read-modify-write durumu tam olarak bu. Düzeltme, sayacı kendi mutex'iyle korumak:
// Service sipariş oluşturur. Eşzamanlı kullanıma uygundur.
type Service struct {
repo Repository
books Catalog
now func() time.Time
mu sync.Mutex // nextID'yi korur
nextID int
}
// newID bir sonraki sipariş ID'sini döndürür. Sadece sayaç kilitlenir;
// katalog sorguları ve repository çağrısı kilidin dışında kalır.
func (s *Service) newID() string {
s.mu.Lock()
defer s.mu.Unlock()
s.nextID++
return fmt.Sprintf("ORD-%d", s.nextID)
}
go run -race ./labs/08-orders
orders placed: 100, unique IDs: 100
Kilidi sadece sayacın etrafında tuttum. Bütün Place'i kilitleseydim katalog sorguları ve repository çağrısı da sıraya girer, 100 sipariş tek tek işlenirdi. Race detector'ın iki sınırı var. Sadece o çalıştırmada gerçekten olan race'leri bulur; kodun hiç çalışmayan yolları için bir şey söyleyemez. Bedeli de yüksek: Go dokümanına göre bellek kullanımı 5-10 kat, çalışma süresi 2-20 kat artabilir. Bu yüzden production binary'sinde değil, testlerde ve CI'da açılır. Windows'ta -race cgo'nun açık olmasını ve mingw-w64 tabanlı bir C derleyicisinin kurulu olmasını istiyor; macOS'ta C derleyicisi gerekmiyor.
context.Context: İptal, Deadline ve Değer
07'nin sonunda bir soru kalmıştı: bir isteğin artık beklenmediğini goroutine'lere nasıl söyleriz? Go'nun cevabı context.Context. Bir context iptal edilebilir, bir deadline taşıyabilir ve istek boyunca küçük değerleri (request ID gibi) taşıyabilir. C# Asenkron Programlama serisinin "C# CancellationToken: İptal, Timeout ve Linked Token" yazısındaki CancellationToken ile ASP.NET Core'daki HttpContext.Items'ın birleşimi gibi düşünebilirsin.
// Lab 08: context.Context ile iptal, deadline, cause ve değer taşıma.
package main
import (
"context"
"errors"
"fmt"
"time"
"bookstore/internal/catalog"
)
// requestIDKey export edilmiyor, bu yüzden başka bir package aynı key'i kullanamaz.
type requestIDKey struct{}
// lookupStock, delay kadar süren bir envanter çağrısının yerini tutar.
// ctx biter bitmez vazgeçer.
func lookupStock(ctx context.Context, isbn string, delay time.Duration) (int, error) {
rid, _ := ctx.Value(requestIDKey{}).(string)
select {
case <-time.After(delay):
b, err := catalog.Store{}.ByISBN(isbn)
return b.Stock, err
case <-ctx.Done():
return 0, fmt.Errorf("request %s: lookup %s: %w", rid, isbn, context.Cause(ctx))
}
}
func main() {
base := context.WithValue(context.Background(), requestIDKey{}, "req-42")
// 1) ve 2) 100 ms'lik deadline: önce yeterince hızlı, sonra fazla yavaş.
for _, delay := range []time.Duration{50 * time.Millisecond, 300 * time.Millisecond} {
ctx, cancel := context.WithTimeout(base, 100*time.Millisecond)
start := time.Now()
stock, err := lookupStock(ctx, "9780441172719", delay)
cancel() // başarılı olsa bile timer'ı her zaman serbest bırak
fmt.Printf("delay %v: stock=%d err=%v (%v)\n", delay, stock, err,
time.Since(start).Round(10*time.Millisecond))
fmt.Println(" deadline exceeded?", errors.Is(err, context.DeadlineExceeded))
}
// 3) Bir sebeple (cause) iptal etmek.
ctx, cancel := context.WithCancelCause(base)
go func() {
time.Sleep(20 * time.Millisecond)
cancel(errors.New("client closed the connection"))
}()
_, err := lookupStock(ctx, "9780141439587", time.Second)
fmt.Println(err)
fmt.Println(" ctx.Err():", ctx.Err())
// 4) İptal aşağı doğru akar: iptal edilmiş bir context'in child'ı anında biter.
child, stop := context.WithTimeout(ctx, time.Hour)
defer stop()
<-child.Done()
fmt.Println("child:", child.Err(), "| cause:", context.Cause(child))
}
go run ./labs/08-context
delay 50ms: stock=12 err=<nil> (50ms)
deadline exceeded? false
delay 300ms: stock=0 err=request req-42: lookup 9780441172719: context deadline exceeded (100ms)
deadline exceeded? true
request req-42: lookup 9780141439587: client closed the connection
ctx.Err(): context canceled
child: context canceled | cause: client closed the connection
Üç kural Go'da konvansiyon hâline gelmiş durumda ve context package'ının dokümanında da yazıyor. Context, ihtiyaç duyan her fonksiyona ilk parametre olarak ve ctx adıyla verilir; 04'ten beri Repository.Save(ctx, o) ve Service.Place(ctx, ...) böyle. Context bir struct'ın field'ında saklanmaz. WithTimeout ve WithCancel'ın döndürdüğü cancel her zaman çağrılır; çağırmazsan timer ve context ağacındaki bağlantı serbest kalmaz, go vet'in lostcancel kontrolü de bunu yakalar.
Lab'ın son satırı iptalin yönünü gösteriyor: iptal parent'tan child'a akıyor, tersine değil. context.Cause (Go 1.20) ise "neden iptal edildi?" sorusuna context canceled'dan daha iyi bir cevap veriyor. Değer taşırken export edilmeyen bir key tipi kullan ve context'i fonksiyon parametrelerinin yerine kullanma; sadece isteğe ait, sınırlar arasında geçen veriler (request ID, kimlik bilgisi) için. HTTP server'da her isteğin context'i r.Context() ile gelir ve istemcinin bağlantısı kapanınca iptal edilir; 09'da handler'lardan service katmanına bunu taşıyacağız.
WaitGroup.Go, sync.Once ve atomic
Lab'daki wg.Go(func() { ... }) Go 1.25'te geldi ve wg.Add(1), go ve defer wg.Done() üçlüsünü tek çağrıya indiriyor. Eski kalıbın en sık hatası Add'i goroutine'in içinde çağırmaktı; Go method'u bu hatayı imkânsız hâle getiriyor. 1.27'nin go fix'indeki waitgroupgo modernizer'ı eski kalıbı bu yeni çağrıya çeviriyor.
İki küçük yardımcı daha: sync.OnceValue (Go 1.21), bir değeri ilk ihtiyaç anında tek bir kez hesaplayan fonksiyon döndürüyor; fiyat listesini ilk istekte yüklemek gibi işler için C#'taki Lazy<T>'nin karşılığı. sync/atomic'teki atomic.Int64 gibi tipler (Go 1.19) ise tek bir sayaç için mutex'ten daha hafif; Interlocked.Increment'ın karşılığı. nextID'yi atomic.Int64 ile de yazabilirdim; mutex'i seçtim, çünkü ileride ID'nin yanında başka bir alanı da aynı kilitle korumak isteyebiliriz. Aşağıdaki lab'da sayaçlar için atomic tipleri kullandım.
errgroup ile Worker Pool: 10.000 Fatura
Ay sonunda Bookstore'un 10.000 sipariş için fatura PDF'i üretmesi gerekiyor. Hepsini sırayla yapmak yavaş, hepsini aynı anda başlatmak ise PDF kütüphanesini, diski ve veritabanını bir anda yükler. golang.org/x/sync/errgroup bu iki ihtiyacı birlikte karşılıyor: SetLimit ile aynı anda çalışan goroutine sayısını sınırlıyor, WithContext ile ilk hatada ortak context'i iptal ediyor ve Wait ilk hatayı döndürüyor. C#'taki Task.WhenAll ile SemaphoreSlim'in birlikte yaptığı iş.
go get golang.org/x/sync@v0.25.0
// Lab 08: errgroup, limit ve iptal ile 10.000 fatura üretmek.
package main
import (
"context"
"fmt"
"sync/atomic"
"time"
"golang.org/x/sync/errgroup"
)
// render, yaklaşık 1 ms süren tek bir fatura PDF'i üretiminin yerini tutar.
func render(ctx context.Context, orderID, failAt int) error {
if err := ctx.Err(); err != nil {
return err // başka bir fatura hata verdi: yeni iş başlatma
}
time.Sleep(time.Millisecond)
if orderID == failAt {
return fmt.Errorf("render invoice ORD-%d: template not found", orderID)
}
return nil
}
func run(name string, limit, failAt int) {
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(limit) // -1 limit yok demek
var rendered, inFlight, peak atomic.Int64
start := time.Now()
for id := 1; id <= 10_000; id++ {
g.Go(func() error { // limit kadar goroutine çalışırken bloklar
n := inFlight.Add(1)
defer inFlight.Add(-1)
for { // aynı anda üretilen en yüksek fatura sayısını kaydet
p := peak.Load()
if n <= p || peak.CompareAndSwap(p, n) {
break
}
}
if err := render(ctx, id, failAt); err != nil {
return err
}
rendered.Add(1)
return nil
})
}
err := g.Wait() // bütün goroutine'ler döndükten sonra ilk hata
fmt.Printf("%-12s rendered=%5d peak=%5d time=%-7v err=%v\n", name, rendered.Load(), peak.Load(),
time.Since(start).Round(time.Millisecond), err)
}
func main() {
run("limit 8", 8, 0)
run("limit 8+err", 8, 5000)
run("no limit", -1, 0)
}
go run ./labs/08-invoices
limit 8 rendered=10000 peak= 8 time=1.575s err=<nil>
limit 8+err rendered= 5006 peak= 8 time=788ms err=render invoice ORD-5000: template not found
no limit rendered=10000 peak= 5313 time=5ms err=<nil>
Limit 8 iken aynı anda en fazla 8 fatura üretildi. İkinci çalıştırmada 5000 numaralı fatura hata verdi; context iptal edildi, o sırada işte olan birkaç fatura bitti ve geri kalanlar ctx.Err() kontrolünde hiç başlamadan döndü. Limitsiz çalıştırma 5 ms sürdü ama aynı anda 5313 fatura üretiliyordu. Bu lab'daki time.Sleep hiçbir kaynak tüketmiyor; gerçek bir PDF kütüphanesi ya da veritabanı connection pool'u bu yükü kaldırmaz. Saniyede en fazla N istek gibi bir hız sınırı gerekiyorsa golang.org/x/time/rate package'ı token bucket algoritmasıyla bunu sağlıyor.
Goroutine Leak'ini Bulmak: goroutineleak Profili
07'deki firstMissing, her çağrıda üç goroutine sızdırıyordu ve bunu runtime.NumGoroutine'in artışından anlamıştık. Gerçek bir serviste hangi satırın sızdırdığını bulmak için Go 1.27'de runtime/pprof'a giren goroutineleak profili var. Release notes'a göre runtime, garbage collector'ın erişilebilirlik bilgisini kullanıyor: bir goroutine'in beklediği channel ya da mutex'e çalışabilecek hiçbir goroutine'den ulaşılamıyorsa o goroutine bir daha uyanamaz, yani sızmıştır.
func main() {
cart := []string{"9780000000000", "9780441172719", "9780547928227", "9780141439549"}
for range 3 {
_ = firstMissing(cart)
}
time.Sleep(100 * time.Millisecond) // göndericilerin bloklanan send'e ulaşmasını bekle
if err := pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1); err != nil {
fmt.Println(err)
}
}
go run ./labs/08-leakprofile
goroutineleak profile: total 9
9 @ 0x102447f78 0x1023e2224 0x1023e1e38 0x10249bae4 0x10244e084
# 0x10249bae3 main.firstMissing.func1+0x63 /home/user/bookstore/labs/08-leakprofile/main.go:25
Üç çağrıda sızan dokuz goroutine'in hepsi aynı satırda, 25. satırdaki errs <- err'de bekliyor. Release notes bir sınır da belirtiyor: channel global bir değişkenden ya da çalışan bir goroutine'in local değişkeninden erişilebiliyorsa runtime bunu leak olarak göremeyebilir. Profili 13'te net/http/pprof ile ayrı bir port'tan sunacağız; aynı profil orada /debug/pprof/goroutineleak adresinde.
testing/synctest ile Zamana Bağlı Testler
Bookstore'da ödenmeyen bir sipariş 15 dakika sonra iptal ediliyor. Bu kuralı gerçek saatle test etmek 15 dakika sürer. Go 1.25'te genel kullanıma açılan testing/synctest, testi bir "bubble" içinde çalıştırıyor: bubble içindeki time fonksiyonları sahte bir saat kullanıyor ve bubble'daki bütün goroutine'ler bloklandığında saat anında ileri atlıyor. Test yazmayı 12'de ayrıntılı göreceğiz; burada sadece bu lab'ın testi var.
// awaitPayment, paid kapanana ya da timeout dolana kadar bekler.
func awaitPayment(ctx context.Context, paid <-chan struct{}, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
select {
case <-paid:
return nil
case <-ctx.Done():
return fmt.Errorf("await payment: %w", ctx.Err())
}
}
package main
import (
"context"
"errors"
"testing"
"testing/synctest"
"time"
)
func TestAwaitPaymentTimesOut(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
start := time.Now() // bubble içinde sahte saat
err := awaitPayment(t.Context(), make(chan struct{}), 15*time.Minute)
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("err = %v, want DeadlineExceeded", err)
}
if waited := time.Since(start); waited != 15*time.Minute {
t.Fatalf("waited %v, want exactly 15m", waited)
}
})
}
func TestAwaitPaymentPaidInTime(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
paid := make(chan struct{})
go func() {
time.Sleep(14 * time.Minute)
close(paid)
}()
if err := awaitPayment(t.Context(), paid, 15*time.Minute); err != nil {
t.Fatalf("err = %v, want nil", err)
}
})
}
go test -race -v ./labs/08-payment
=== RUN TestAwaitPaymentTimesOut
--- PASS: TestAwaitPaymentTimesOut (0.00s)
=== RUN TestAwaitPaymentPaidInTime
--- PASS: TestAwaitPaymentPaidInTime (0.00s)
PASS
ok bookstore/labs/08-payment 1.228s
İki test de 0,00 saniyede geçti ve ilk test sahte saatte tam 15 dakikanın geçtiğini doğruladı. Toplam sürenin 1,2 saniye olması race detector'lı binary'nin derlenip başlamasından. go test'e -race eklemek, concurrency içeren her test için ucuz bir sigorta.
Üretimde Dikkat
Kilidin içinde I/O yapma. Mutex'i tutarken bir HTTP çağrısı ya da veritabanı sorgusu yaparsan, o çağrı ne kadar sürerse diğer bütün istekler o kadar bekler. newID'nin sadece sayacı kilitlemesi bu yüzden.
memstore hâlâ sakladığı pointer'ı döndürüyor. Map artık güvenli, ama iki goroutine aynı *Order üzerinde MarkPaid çağırırsa yine bir data race olur. Bellekte çalışan bir store için ya kopya saklayıp kopya döndürmek ya da durum değişikliklerini store'un kilidi altında yapmak gerekir. 11'deki sqlstore her okumada yeni bir struct ürettiği için bu sorunu taşımıyor.
Race detector'ı CI'a koy. go test -race ./...'i her pull request'te çalıştırmak, bu yazıdaki gibi sessiz ID çakışmalarını production'a çıkmadan yakalar. Ama sadece testlerin gerçekten eşzamanlı çalıştırdığı kodu kontrol eder; eşzamanlı bir test yazmadan race detector'dan bir şey bekleme.
Sık Düşülen Tuzaklar
Mutex'i kopyalamak ve Add'i goroutine içinde çağırmak. İkisi de derlenir ve çoğu zaman çalışıyor gibi görünür; go vet ikisini de yakalıyor:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
// Value'nun receiver'ı value: her çağrı, mutex dahil c'nin bir kopyasıyla çalışır.
func (c Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
var c Counter
var wg sync.WaitGroup
for range 10 {
go func() {
wg.Add(1) // çok geç: Wait çoktan dönmüş olabilir
defer wg.Done()
c.mu.Lock()
c.n++
c.mu.Unlock()
}()
}
wg.Wait()
fmt.Println(c.Value())
}
go vet .
go run .
main.go:14:9: Value passes lock by value: vetdemo.Counter contains sync.Mutex
main.go:25:10: WaitGroup.Add called from inside new goroutine
2
Program 10 yerine 2 yazdı: Wait, goroutine'lerin çoğu Add'e ulaşmadan döndü. Mutex içeren bir struct'ın method'larında pointer receiver kullan ve WaitGroup.Go'yu tercih et.
Context'i struct'ta saklamak. Service'in field'ına konan bir context, ilk isteğin ömrünü bütün isteklere taşır; iptal edilmiş bir context'le sonraki her çağrı anında hata verir. Context her çağrıda parametre olarak gelir.
cancel'ı çağırmamak. WithTimeout başarıyla bitse bile defer cancel() yaz; yoksa timer deadline'a kadar yaşar.
Context'i dinlemeyen goroutine. ctx'i parametre olarak alıp hiç ctx.Done()'a bakmayan bir fonksiyon iptali görmez. Uzun süren her döngü ya da bekleme, select ile ya da ctx.Err() kontrolüyle context'i dinlemeli.
Küçük Bir Deneme
★☆☆ Service'teki mutex'i silip nextID'yi atomic.Int64 yap ve go run -race ./labs/08-orders ile race detector'ın sessiz kaldığını doğrula.
★★☆ labs/08-invoices'taki render'dan ctx.Err() kontrolünü sil. Hata sonrası kaç fatura üretildi ve çalıştırma ne kadar sürdü? Sonucu açıkla.
★★★ memstore.Store'u, sakladığı siparişin bir kopyasını döndürecek şekilde değiştir (Items slice'ını da kopyalamayı unutma). Sonra iki goroutine'in aynı siparişi okuyup MarkPaid çağırdığı bir lab yaz ve -race ile değişiklikten önceki ve sonraki hâli karşılaştır.
Concurrency bloğu burada bitiyor. Bir sonraki yazıda HTTP katmanına dönüyoruz: net/http'nin ServeMux'ındaki pattern routing ve precedence kuralları, middleware zinciri kurmak, request ID ve logging middleware'leri, http.ListenAndServe yerine timeout'ları ayarlanmış bir http.Server ve http.CrossOriginProtection.