Mert Tosun
← Yazılar
Go Routine'lerin Gücü: Eşzamanlılığı Doğru Kullanmak

Go Routine'lerin Gücü: Eşzamanlılığı Doğru Kullanmak

Mert TosunGo

Go'yu diğer dillerden ayıran en belirgin özellik, eşzamanlılığı (concurrency) birinci sınıf vatandaş olarak ele alış biçimidir. Thread yerine goroutine, mutex yerine channel tercih eden Go, "belleği paylaşarak iletişim kurma; iletişim kurarak belleği paylaş" felsefesini kodun merkezine yerleştirir.

Bu yazıda goroutine'lerin nasıl çalıştığını, hangi durumlarda ne pattern kullanılacağını ve üretim kodunda dikkat edilmesi gereken tuzakları ele alacağız.

Goroutine Nedir?

Goroutine, Go çalışma zamanı (runtime) tarafından yönetilen hafif bir yürütme birimidir. İşletim sistemi thread'lerinden çok daha ucuzdur:

OS Thread Goroutine
Başlangıç maliyeti ~1 MB stack ~2-8 KB stack
Oluşturma süresi ~10 µs ~300 ns
Context switch Kernel mode Kullanıcı modu
Yönetim İşletim sistemi Go runtime

Bir goroutine başlatmak tek satır:

go func() {
    fmt.Println("Ben ayrı bir goroutine'deyim")
}()

Go runtime, bu goroutine'leri M:N scheduling ile mevcut CPU çekirdeklerine dağıtır. Binlerce goroutine çalıştırmak tamamen normaldir.

Channel: Goroutine'ler Arası İletişim

Channel'lar, goroutine'lerin birbirleriyle güvenle veri paylaşmasını sağlar. İki türü vardır:

// Unbuffered channel — gönderen, alıcı hazır olana kadar bekler
ch := make(chan int)

// Buffered channel — tampon dolana kadar gönderen beklemez
ch := make(chan int, 10)

Temel kullanım:

func topla(a, b int, sonuc chan<- int) {
    sonuc <- a + b
}

func main() {
    ch := make(chan int)
    go topla(3, 5, ch)
    fmt.Println(<-ch) // 8
}

Pattern 1: Worker Pool

En yaygın pattern. Sabit sayıda worker goroutine iş kuyruğundan görev alır. CPU veya I/O kaynaklarını belirli bir sınırda tutar.

package main

import (
    "fmt"
    "sync"
)

func workerPool(islemSayisi, workerSayisi int) {
    isler := make(chan int, islemSayisi)
    var wg sync.WaitGroup

    // Worker'ları başlat
    for w := 0; w < workerSayisi; w++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for is := range isler {
                // İşi yap
                sonuc := is * is
                fmt.Printf("Worker %d: %d^2 = %d\n", id, is, sonuc)
            }
        }(w)
    }

    // İşleri kuyruğa ekle
    for i := 1; i <= islemSayisi; i++ {
        isler <- i
    }
    close(isler) // Kanal kapanınca worker'lar for-range'den çıkar

    wg.Wait()
    fmt.Println("Tüm işler tamamlandı")
}

func main() {
    workerPool(20, 4) // 20 iş, 4 worker
}

Ne zaman kullanılır? Veritabanı sorguları, HTTP istekleri, dosya işlemleri gibi I/O ağırlıklı görevlerde eşzamanlılığı kontrol etmek için.


Pattern 2: Fan-Out / Fan-In

Bir veri kaynağından gelen işleri birden fazla goroutine'e dağıtıp (fan-out), sonuçları tek bir kanalda birleştir (fan-in).

package main

import (
    "fmt"
    "sync"
)

// Tek bir kanaldan okuyup işi birden fazla goroutine'e dağıt
func fanOut(giris <-chan int, workerSayisi int) []<-chan int {
    cikislar := make([]<-chan int, workerSayisi)
    for i := 0; i < workerSayisi; i++ {
        cikis := make(chan int)
        cikislar[i] = cikis
        go func(c chan<- int) {
            for v := range giris {
                c <- v * v // kare hesapla
            }
            close(c)
        }(cikis)
    }
    return cikislar
}

// Birden fazla kanalı tek bir çıkış kanalında birleştir
func fanIn(kanallar ...<-chan int) <-chan int {
    birlesmis := make(chan int)
    var wg sync.WaitGroup

    topla := func(c <-chan int) {
        defer wg.Done()
        for v := range c {
            birlesmis <- v
        }
    }

    wg.Add(len(kanallar))
    for _, c := range kanallar {
        go topla(c)
    }

    go func() {
        wg.Wait()
        close(birlesmis)
    }()

    return birlesmis
}

func main() {
    giris := make(chan int, 10)
    for i := 1; i <= 10; i++ {
        giris <- i
    }
    close(giris)

    cikislar := fanOut(giris, 3)
    sonuclar := fanIn(cikislar...)

    for s := range sonuclar {
        fmt.Println(s)
    }
}

Ne zaman kullanılır? Büyük veri setlerini paralel işlerken ya da farklı API'lara eşzamanlı istek atarken.


Pattern 3: Pipeline

Veriyi bir işleme zincirinden geçir. Her aşama bir goroutine, aşamalar kanallarla birbirine bağlı.

package main

import "fmt"

// Aşama 1: Sayı üret
func uret(sayilar ...int) <-chan int {
    cikis := make(chan int)
    go func() {
        for _, n := range sayilar {
            cikis <- n
        }
        close(cikis)
    }()
    return cikis
}

// Aşama 2: Karelerini al
func karele(giris <-chan int) <-chan int {
    cikis := make(chan int)
    go func() {
        for n := range giris {
            cikis <- n * n
        }
        close(cikis)
    }()
    return cikis
}

// Aşama 3: Sadece çiftleri filtrele
func ciftFiltre(giris <-chan int) <-chan int {
    cikis := make(chan int)
    go func() {
        for n := range giris {
            if n%2 == 0 {
                cikis <- n
            }
        }
        close(cikis)
    }()
    return cikis
}

func main() {
    // Pipeline: üret → karele → filtrele
    sayilar := uret(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    kareler := karele(sayilar)
    ciftler := ciftFiltre(kareler)

    for v := range ciftler {
        fmt.Println(v) // 4, 16, 36, 64, 100
    }
}

Ne zaman kullanılır? ETL süreçleri, log işleme, görüntü/ses dönüştürme gibi veriyi adım adım dönüştüren senaryolarda.


Pattern 4: Context ile İptal

Uzun süren goroutine'leri kontrollü şekilde durdurmak için context paketi kullanılır. Bu, production kodunun vazgeçilmezidir.

package main

import (
    "context"
    "fmt"
    "time"
)

func uzunIslem(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d iptal edildi: %v\n", id, ctx.Err())
            return
        default:
            // İş yap
            fmt.Printf("Worker %d çalışıyor...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // 2 saniye sonra iptal edecek context
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    for i := 1; i <= 3; i++ {
        go uzunIslem(ctx, i)
    }

    <-ctx.Done()
    fmt.Println("Ana program sona erdi")
    time.Sleep(100 * time.Millisecond) // log'ların yazılması için bekle
}

Ne zaman kullanılır? HTTP handler'larında istek iptallerini yakalamak, deadline'lı database sorgularında, servis kapatma sinyallerini iletmek için.


Pattern 5: Semaphore ile Eşzamanlılık Sınırı

Buffered channel kullanarak aynı anda çalışan goroutine sayısını sınırla:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    const maxEsZamanli = 3
    semaphore := make(chan struct{}, maxEsZamanli)

    var wg sync.WaitGroup

    for i := 1; i <= 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            semaphore <- struct{}{} // Slot al
            defer func() { <-semaphore }() // Slot bırak

            fmt.Printf("Görev %d başladı\n", id)
            time.Sleep(1 * time.Second)
            fmt.Printf("Görev %d bitti\n", id)
        }(i)
    }

    wg.Wait()
}

Bu pattern, Worker Pool'dan farklı olarak goroutine'lerin dinamik oluşturulmasına izin verirken eşzamanlılığı kontrol eder.


Sık Yapılan Hatalar

1. Goroutine Sızıntısı (Goroutine Leak)

// YANLIŞ: channel'dan hiç okunmazsa goroutine sonsuza kadar bloklanır
func yanlis() {
    ch := make(chan int)
    go func() {
        ch <- 42 // Hiç okunmazsa buraya takılır!
    }()
    // ch'dan hiç okumuyoruz, goroutine asılı kaldı
}

// DOĞRU: context veya done channel ile çıkış garantile
func dogru(ctx context.Context) {
    ch := make(chan int, 1) // Buffered veya
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done(): // context iptalinde çık
        }
    }()
}

2. WaitGroup'u Yanlış Kullanmak

// YANLIŞ: Add(1) goroutine içinde çağrılırsa race condition oluşur
for i := 0; i < 5; i++ {
    go func() {
        wg.Add(1) // HATALI — goroutine başlamadan önce çağrılmalı
        defer wg.Done()
        // iş...
    }()
}

// DOĞRU
for i := 0; i < 5; i++ {
    wg.Add(1) // Goroutine başlamadan önce
    go func() {
        defer wg.Done()
        // iş...
    }()
}

3. Döngü Değişkenini Goroutine'e Geçirmek

// YANLIŞ: Tüm goroutine'ler son 'i' değerini görür
for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i) // Büyük ihtimalle hep 5 basar
    }()
}

// DOĞRU: Değeri parametre olarak geç
for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n) // 0,1,2,3,4
    }(i)
}

Race Condition Tespiti

Go'nun yerleşik race detector'ı üretim hatalarını bulmada çok etkilidir:

go run -race main.go
go test -race ./...
go build -race -o myapp

Race condition tespit edildiğinde şu formatta çıktı verir:

WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 7:
  main.main.func1()
      /tmp/main.go:15 +0x3c
Read at 0x00c0000b4010 by goroutine 8:
  main.main.func2()
      /tmp/main.go:21 +0x3c

Özet: Hangi Pattern Ne Zaman?

Pattern Kullanım Senaryosu
Worker Pool Sabit kaynak kullanımıyla paralel iş işleme
Fan-Out/Fan-In Tek kaynak → paralel işlem → tek sonuç
Pipeline Adım adım veri dönüşümü
Context İptali Timeout, deadline, graceful shutdown
Semaphore Dinamik goroutine'lerde eşzamanlılık sınırı

Go'nun eşzamanlılık modeli, doğru kullanıldığında hem anlaşılır hem de yüksek performanslı sistemler inşa etmenizi sağlar. Temel kural şu: goroutine ucuz, ama sonsuz değil. Her goroutine'in net bir çıkış yolu olmalı.