# Go'da eşzamanlılık: goroutine ve channel pratiği

> Go'nun eşzamanlılık modelini — goroutine ve channel'ı — uygulamalı kavramak. PHP'nin süreç modelinden gelince ne değişiyor?

- Yayın: 2019-11-03
- Güncelleme: 2026-09-07
- Kategori: Diller
- Etiketler: Go
- Okuma süresi: 3 dk okuma
- Kaynak: https://muhammetsafak.com/tr/blog/goda-eszamanlilik-goroutine-ve-channel-pratigi/
- Dil: tr-TR
- Yazar: Muhammet Şafak

---
[Go öğrenmeye başladığımdan](/tr/blog/polyglota-ilk-adim-neden-go-ogrenmeye-basladim/) beri beklediğim konu buydu: concurrency. PHP'de paralel iş yürütmek için ya birden fazla process açarsınız ya da bir kuyruk sistemi kullanırsınız. Go'nun bu problemi dil düzeyinde nasıl ele aldığını görmek istiyordum.

Birkaç haftadır goroutine ve channel ile uğraşıyorum. Teorik anlayışla pratik kullanım arasındaki mesafeyi kapatmak zaman alıyor; ama zihin modelim oturmaya başladı.

## Goroutine nedir?

Goroutine, Go'nun hafif eşzamanlı yürütme birimidir. Bir işletim sistemi thread'i değil — [Effective Go'nun anlattığı gibi](https://go.dev/doc/effective_go#goroutines) goroutine'ler birden fazla OS thread'i üzerine multiplekslenir; biri örneğin I/O beklerken bloklandığında diğerleri çalışmaya devam eder. Başlatmak için `go` anahtar kelimesi yeterli:

```go
package main

import (
    "fmt"
    "time"
)

func selamlama(isim string) {
    fmt.Printf("Merhaba, %s!\n", isim)
}

func main() {
    go selamlama("Ali")
    go selamlama("Ayşe")
    go selamlama("Mehmet")

    // Ana goroutine bitmeden diğerleri çalışsın:
    time.Sleep(100 * time.Millisecond)
}
```

Bu örnek çalışıyor ama `time.Sleep` ile beklemek kötü bir pratik. Goroutine'lerin bitmesini `sync.WaitGroup` ile beklemek daha doğru:

```go
package main

import (
    "fmt"
    "sync"
)

func isGor(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("İş %d tamamlandı\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go isGor(i, &wg)
    }

    wg.Wait()
    fmt.Println("Tüm işler bitti.")
}
```

`defer wg.Done()` goroutine bitince `WaitGroup` sayacını düşürüyor. `wg.Wait()` ise sayaç sıfıra inene kadar bloklıyor.

### Goroutine sızıntısı — sık yapılan bir hata

PHP'den gelince goroutine'lerin "otomatik kapandığını" sanmak kolay. Öyle değil. Bir goroutine hiç bitmeyecek bir kanaldan okuma veya yazma bekliyorsa sonsuza kadar çalışmaya devam eder — siz farkında olmasanız bile. Buna **goroutine leak** deniyor.

```go
// Tehlikeli: goroutine asla bitmez
go func() {
    result := <-ch // ch hiç kapatılmazsa burada takılır
    fmt.Println(result)
}()
```

Bunun önüne geçmenin pratik yolu `context.Context` kullanmak veya kanalı doğru kapatmak. İlk birkaç haftada bu tuzağa birkaç kez düştüm; goroutine sayısını [`runtime.NumGoroutine()`](https://pkg.go.dev/runtime#NumGoroutine) ile izlemek — o an var olan goroutine sayısını döndürüyor — sorunu fark ettirdi.

## Channel: goroutine'ler arası iletişim

Channel, goroutine'ler arasında veri iletmek için kullanılan Go'ya özgü bir yapıdır. "Belleği paylaşarak iletişim kur" yerine "iletişim kurarak bellek paylaş" felsefesi var burada — [Effective Go bunu tek cümleye indiriyor](https://go.dev/doc/effective_go#sharing): "Do not communicate by sharing memory; instead, share memory by communicating."

```go
package main

import "fmt"

func topla(sayilar []int, sonuc chan<- int) {
    toplam := 0
    for _, n := range sayilar {
        toplam += n
    }
    sonuc <- toplam // Channel'a gönder
}

func main() {
    sayilar := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    sonuc := make(chan int)

    // İki goroutine, listeyi ikiye bölerek topluyor
    go topla(sayilar[:5], sonuc)
    go topla(sayilar[5:], sonuc)

    // İki sonucu al
    a, b := <-sonuc, <-sonuc
    fmt.Println("Toplam:", a+b) // 55
}
```

Channel'lar varsayılan olarak unbuffered'dır. [Dil spesifikasyonunun ifadesiyle](https://go.dev/ref/spec#Channel_types), kapasite sıfırsa ya da hiç verilmemişse kanal tamponsuzdur ve iletişim ancak gönderen ile alıcının ikisi de hazır olduğunda gerçekleşir. Bu senkronizasyonu garanti eder.

Buffered channel ise belirli sayıda değeri alıcı olmadan tutabilir:

```go
ch := make(chan int, 3) // 3 değer tutabilir
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // Bu satır bloklardı — kanal dolu
```

Tamponlu kanal bir tür queue gibi davranıyor. Üretici-tüketici senaryolarında faydalı; ama tampon büyüklüğünü yanlış seçmek gizli tıkanmalara yol açabiliyor. Ben genelde önce tamponsuzu deniyor, gerçek bir performans sorunu görürsem tampon ekliyorum.

## select: birden çok channel'ı dinlemek

`select` ifadesi, birden fazla channel'ı eş zamanlı dinlemeye yarıyor. Hangisi hazırsa oradan okuyor:

```go
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "bir"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "iki"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println("ch1'den:", msg)
        case msg := <-ch2:
            fmt.Println("ch2'den:", msg)
        }
    }
}
```

`select` bloğuna `default` case eklenebiliyor. [Spesifikasyon şöyle diyor](https://go.dev/ref/spec#Select_statements): hiçbir iletişim ilerleyemiyorsa ve bir `default` case varsa o case seçilir; `default` yoksa `select` en az bir iletişim ilerleyebilene kadar bloklar. Bu, "varsa oku, yoksa geç" kalıbını uygulamak için kullanışlı.

## PHP ile karşılaştırmak

PHP'de paralel iş yapmak için `pcntl_fork()`, Swoole gibi extension'lar ya da harici bir kuyruk sistemi gerekiyor. Bunların hiçbiri dil seviyesinde değil. Go'da goroutine ve channel, dilin kendisine yerleşik — standart kütüphane dışında hiçbir şey gereksiz.

Bu fark en çok şurada hissediliyor: Go'da eşzamanlı kod yazmak *alışılmamış* ama *olağan*. PHP'de eşzamanlılık, özel durumlara özgü hissettiriyor. Bir PHP dosyasını tarayıcı çağırdığında her istek kendi dünyasında çalışır, başkasının durumuna dokunmaz; bu izolasyon hem kolaylık hem de kısıtlamadır. Go'da ise birden fazla goroutine aynı belleğe erişebilir; bu güç ama dikkat gerektirir.

Hâlâ öğrenme sürecindeyim. Race condition, goroutine leak, channel'ların doğru kapatılması gibi konular dikkat gerektiriyor. Ama zihin modeli netleşti: goroutine ucuz, iletişim channel üzerinden, paylaşılan durum minimize edilmeli. Bu üç ilkeyi aklımda tuttuğumda kodun nereye gitmesi gerektiği çoğunlukla kendiliğinden ortaya çıkıyor.

Bir de concurrency ile parallelism ayrımını anlamak gerekiyor. Go'daki goroutine'ler eşzamanlı çalışır; aynı anda fiziksel olarak paralel çalışıp çalışmadıkları CPU çekirdek sayısına ve [`GOMAXPROCS`](https://pkg.go.dev/runtime#GOMAXPROCS) ayarına bağlı — bu değer aynı anda kaç CPU'nun kod yürütebileceğini belirliyor. PHP'den gelince bu farkı görmek kolay değil — PHP'de "aynı anda birden fazla şey yap" kavramının karşılığı doğrudan süreç açmaktır. Go'da ise eşzamanlılık dil düzeyinde bir tasarım kararı; işi verimli sıralamak ile gerçekten paralel yürütmek ayrı katmanlar. Ayrımın kanonik anlatımı [Rob Pike'ın "Concurrency is not parallelism" konuşması](https://go.dev/blog/waza-talk): eşzamanlılık bağımsız yürüyen süreçlerin *bileşimi*, paralellik ise hesaplamaların *aynı anda yürütülmesi*.

---

### Kaynaklar

- [Effective Go: Goroutines](https://go.dev/doc/effective_go#goroutines)
- [Effective Go: Share by communicating](https://go.dev/doc/effective_go#sharing)
- [The Go Programming Language Specification: Channel types](https://go.dev/ref/spec#Channel_types)
- [The Go Programming Language Specification: Select statements](https://go.dev/ref/spec#Select_statements)
- [Go Packages: runtime — GOMAXPROCS](https://pkg.go.dev/runtime#GOMAXPROCS)
- [Go Packages: runtime — NumGoroutine](https://pkg.go.dev/runtime#NumGoroutine)
- [Rob Pike: Concurrency is not parallelism](https://go.dev/blog/waza-talk)
