1.1 Kanalların Genel Bakışı

Kanal, Go dilinde farklı gorutinler arasındaki iletişim için kullanılan çok önemli bir özelliktir. Go dilinin eşzamansızlık modeli CSP (İletişen Ardışık İşlemler) olduğundan, kanallar mesaj iletişimini sağlar. Kanalların kullanılması, karmaşık bellek paylaşımını önleyerek eşzamanlı program tasarımını daha basit ve daha güvenli hale getirebilir.

1.2 Kanalların Oluşturulması ve Kapatılması

Go dilinde, kanallar make fonksiyonu kullanılarak oluşturulur ve kanalın türü ve tampon boyutu belirtilebilir. Tampon boyutu isteğe bağlıdır ve boyut belirtilmezse tamponsuz bir kanal oluşturulur.

ch := make(chan int)    // türü int olan tamponsuz bir kanal oluştur
chBuffered := make(chan int, 10) // türü int olan 10 kapasiteli bir tamponlu kanal oluştur

Kanalların doğru bir şekilde kapatılması da önemlidir. Veriler artık gönderilmeyecekse, kanalın kapatılması, diğer gorutinlerin veri için sürekli beklemesi veya çıkmaza girmesi durumlarını önlemek için gereklidir.

close(ch) // kanalı kapat

1.3 Veri Gönderme ve Alma

Kanalda veri gönderme ve alma işlemi, <- sembolü kullanılarak gerçekleştirilir. Gönderme işlemi sol tarafta iken, alma işlemi sağ taraftadır.

ch <- 3 // Kanala veri gönder
değer := <- ch // Kanaldan veri al

Ancak, gönderme işlemi, veri alındığı ana kadar bloke olacak ve aynı şekilde, alım işlemi de okunacak veri olana kadar bloke olacaktır.

fmt.Println(<-ch) // ch kanalından veri gelene kadar blok olacak

2. Kanalların İleri Düzey Kullanımı

2.1 Kanal Kapasitesi ve Tamponlama

Kanallar tamponsuz veya tamponlu olabilir. Tamponsuz kanallar, alıcının mesajı alması için göndericinin bloke olmasına neden olur. Tamponsuz kanallar, genellikle iki gorutinin belirli bir anda senkronizasyonunu sağlamak için kullanılır.

ch := make(chan int) // tamponsuz kanal oluştur
go func() {
    ch <- 1 // kanalı alıcı gorutin olmadığı sürece bloke edecek
}()

Tamponlu kanalların bir kapasite limiti vardır ve kanala veri gönderme işlemi sadece tampon dolu olduğunda bloke eder. Benzer şekilde, boş bir tampondan veri almak da bloke eder. Tamponlu kanallar genellikle yüksek trafiği yönetmek ve eşzamansız iletişim senaryoları için kullanılır, bekleme nedeniyle doğrudan performans kaybını azaltır.

ch := make(chan int, 10) // 10 kapasiteli bir tamponlu kanal oluştur
go func() {
    for i := 0; i < 10; i++ {
        ch <- i // kanal zaten doluysa bloke etmeyecek
    }
    close(ch) // gönderme işlemi bittikten sonra kanalı kapat
}()

Kanal türü seçimi, iletişimin doğasına bağlıdır: senkronizasyonun garanti edilip edilmediği, tamponlamanın gerekip gerekmediği ve performans gereksinimleri gibi faktörlere göre belirlenir.

2.2 select İfadesinin Kullanımı

Birden fazla kanal arasında seçim yaparken, select ifadesi çok faydalıdır. Switch ifadesine benzer şekilde, select ifadesi her bir durumda kanal işlemi içerir. Kanallardaki veri akışını dinleyebilir ve aynı anda birden fazla kanal hazır olduğunda, select rastgele birini çalıştırmak için seçim yapar.

ch1 := make(chan int)
ch2 := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch1 <- i
    }
}()

go func() {
    for i := 0; i < 5; i++ {
        ch2 <- i * 10
    }
}()

for i := 0; i < 5; i++ {
    select {
    case v1 := <-ch1:
        fmt.Println("ch1 kanalından alındı:", v1)
    case v2 := <-ch2:
        fmt.Println("ch2 kanalından alındı:", v2)
    }
}

select kullanarak, birden fazla kanaldan aynı anda veri almak veya belirli koşullara göre veri göndermek gibi karmaşık iletişim senaryolarıyla başa çıkılabilir.

2.3 Kanallar için Aralık Döngüsü

range anahtar kelimesini kullanarak kanaldan sürekli veri alınır, ta ki kanal kapatılana kadar. Bu, özellikle üretici-tüketici modelinde bilinmeyen miktarda veri ile uğraşırken çok faydalıdır.

ch := make(chan int)

go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // Kanalın kapatıldığından emin olun
}()

for n := range ch {
    fmt.Println("Alındı:", n)
}

Kanal kapatıldığında ve geriye veri kalmadığında döngü sona erer. Kanal kapatmayı unutursanız, range bir goroutine sızıntısına neden olabilir ve program verinin gelmesi için sürekli bekleyebilir.

3 Eş Zamanlılıkta Karmaşık Durumlarla Başa Çıkma

3.1 Bağlamın Rolü

Go dilinin eş zamanlı programlamasında, context paketi önemli bir rol oynar. Bağlam, tek bir isteği işleyen birden fazla goroutine arasında veri yönetimini, iptal sinyallerini, süreleri vb. basitleştirmek için kullanılır.

Bir web servisinin bir veritabanını sorgulaması ve veriler üzerinde bazı hesaplamalar yapması gereken durumu düşünelim. Bu işlem birden fazla goroutine'da gerçekleştirilmelidir. Eğer bir kullanıcı isteği birdenbire iptal ederse veya servis isteği belirli bir süre içinde tamamlamak zorunda kalırsa, tüm çalışan goroutine'lerin iptal edilmesi için bir mekanizmaya ihtiyacımız vardır.

Bu durumda, bu gereksinimi karşılamak için context kullanırız:

package main

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

func operation1(ctx context.Context) {
	time.Sleep(1 * time.Second)
	select {
	case <-ctx.Done():
		fmt.Println("operation1 iptal edildi")
		return
	default:
		fmt.Println("operation1 tamamlandı")
	}
}

func operation2(ctx context.Context) {
	time.Sleep(2 * time.Second)
	select {
	case <-ctx.Done():
		fmt.Println("operation2 iptal edildi")
		return
	default:
		fmt.Println("operation2 tamamlandı")
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	go operation1(ctx)
	go operation2(ctx)

	<-ctx.Done()
	fmt.Println("ana: bağlam tamamlandı")
}

Yukarıdaki kodda, belirli bir süre sonra otomatik olarak iptal eden bir Bağlam oluşturmak için context.WithTimeout kullanılır. operation1 ve operation2 fonksiyonları, ctx.Done()'u dinleyen bir select bloğuna sahiptir, bu sayede Bağlam bir iptal sinyali gönderdiğinde hemen durabilirler.

3.2 Kanallarla Hata İşleme

Eş zamanlı programlamada, hata işleme önemli bir faktördür. Go'da, hataları goroutine'lerle asenkron bir şekilde işlemek için kanalları kullanabilirsiniz.

Aşağıdaki kod örneği, bir goroutine'den hataları dışarı aktarmayı ve bunları ana goroutine'de işlemeyi nasıl gösterir:

package main

import (
	"errors"
	"fmt"
	"time"
)

func performTask(id int, errCh chan<- error) {
	// Başarılı veya başarısız olabilecek bir görevi taklit et
	if id%2 == 0 {
		time.Sleep(2 * time.Second)
		errCh <- errors.New("görev başarısız oldu")
	} else {
		fmt.Printf("görev %d başarıyla tamamlandı\n", id)
		errCh <- nil
	}
}

func main() {
	görevler := 5
	errCh := make(chan error, görevler)

	for i := 0; i < görevler; i++ {
		go performTask(i, errCh)
	}

	for i := 0; i < görevler; i++ {
		err := <-errCh
		if err != nil {
			fmt.Printf("hata alındı: %s\n", err)
		}
	}
	fmt.Println("tüm görevlerin işlenmesi tamamlandı")
}

Bu örnekte, performTask fonksiyonunu başarılı veya başarısız olabilecek bir görevi taklit etmek için tanımlarız. Hatalar, bir parametre olarak iletilen errCh kanalı aracılığıyla ana goroutine'e gönderilir. Ana goroutine, tüm görevlerin tamamlanmasını bekler ve hata mesajlarını okur. Bir tamponlu kanal kullanarak, gorutinlerin alınmayan hatalardan dolayı engellenmesini önleriz.

Bu teknikler, eş zamanlı programlamadaki karmaşık durumlarla başa çıkmak için güçlü araçlardır. Onları uygun şekilde kullanmak, kodu daha güçlü, anlaşılır ve sürdürülebilir hale getirebilir.