Directional Channels, Yönlü Kanallar

ud-215

Bir önceki kısımda ele aldığımız kanala hem gönderme hem alma yapabiliyorduk. Bunlara bidirectional channel diyoruz. Ama tek yönlü tanımlamalar da yapabiliyoruz. Örneğin bir fonksiyonumuz sadece dinleme yapacaksa bunu yapabiliriz. Bu okunabilirliği arttıracaktır, aynı zamanda yanlşılıkla veri göndermemizin önüne geçecektir. Bunu C’deki const pointer gibi düşünebiliriz. Hem okunabilirliği arttırma hem de kodlama hatalarını yakalamak için iyi bir yöntem. Go’da detayından emin olmasam da belki böyle detaylı tanımlama yapmanın kod optimizasyonuna faydası da vardır.

Önce koda bir bakalım:

package main

import (
	"fmt"
)

func main() {
	c := make(chan int, 1)
	c <- 42
	fmt.Println(<-c)
	fmt.Printf("c'nin türü:\t%T\n", c)

	csend := make(chan<- int, 1)
	csend <- 42
	//Alttaki yorumu kaldırırsak sentaks hatası
	//Çünkü send-only channel'dan veri alamayız
	//fmt.Println(<-csend)
	fmt.Printf("csend'in türü:\t%T\n", csend)

	crx := make(<-chan int, 1)
	//Alttaki yorumu kaldırırsak sentaks hatası
	//Çünkü receive-only channel'a veri koyamayız.
	//crx <- 42

	//Alttaki kodda problem yok ama
	//bu örnekte deadlock oluşturacak.
	//fmt.Println(<-crx)
	fmt.Printf("crx'in türü:\t%T\n", crx)
}

Çıktı:

42
c'nin türü:	chan int
csend'in türü:	chan<- int
crx'in türü:	<-chan int

“Normal” kanalların türü chan int ya da chan <T> olmaktadır. Ssadece gönderim yapılabilen send-only kanalların türü chan<- <T> olmakta, receive-only kanalların ise <-chan int olmaktadır. Koddaki yorumlu satırlardan görebileceğiniz üzere send-only kanallarda dinleme, receive-only kanallarda ise gönderim yapılamaz.

Türler Arası Atamalar

Bu 3 adet tür arasında atama yapılabilir, bellirli kurallar çerçevesinde. Koda ve çıktısına bakalım.

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)
	ctx := make(chan<- int, 1)
	crx := make(<-chan int, 1)
	fmt.Printf("c'nin türü:\t%T\n", c)
	fmt.Printf("ctx'in türü:\t%T\n", ctx)
	fmt.Printf("crx'in türü:\t%T\n", crx)

	// ctx = crx //Hata
	ctx = c //OK
	fmt.Printf("ctx'in türü:\t%T\n", ctx)

	// crx = ctx //Hata
	crx = c //OK
	fmt.Printf("crx'in türü:\t%T\n", crx)

	// c = ctx //Hata
	// c = crx //Hata
}

Çıktı:

c'nin türü:	chan int
ctx'in türü:	chan<- int
crx'in türü:	<-chan int
ctx'in türü:	chan<- int
crx'in türü:	<-chan int

Go, statik tür sistemine sahip olduğu için atama sonrasında tür değişmez. Go’da kanallar aslında referans (pointer gibi, Python’daki tüm atamalar gibi) gösterdiği için iki kanal birbirine atandığı zaman aslında aynı kanalı gösterirler. Görüldüğü üzere “özelden, genele” yani daha özellikli kanaldan çift yönlü “klasik” kanala atama yapılamaz ama “genelden özele” atama yapılabilir.

Aşağıdaki örnekte atama yapılmış iki kanalın aynı kanal olduğu gösterilmiştir.

package main

import (
	"fmt"
)

func main() {
	c1 := make(chan int, 1)
	c2 := make(chan int)
	c2 = c1
	c1 <- 42
	fmt.Println(<-c2)
}

Çıktı:

42

Tür dönüşüm işlemleriyle yine “genelden özele” dönüşüm yapılabilir.

package main

import (
	"fmt"
)

func main() {
	c := make(chan int)
	ctx := make(chan<- int, 1)
	crx := make(<-chan int, 1)
	fmt.Printf("c'nin türü:\t%T\n", c)
	fmt.Printf("ctx'in türü:\t%T\n", ctx)
	fmt.Printf("crx'in türü:\t%T\n", crx)

	fmt.Printf("%T\n", (chan<- int)(c))
	fmt.Printf("%T\n", (<-chan int)(c))
	//fmt.Printf("%T\n", (chan int)(ctx))   //Hata
	//fmt.Printf("%T\n", (<-chan int)(ctx)) //Hata
	//fmt.Printf("%T\n", (chan<- int)(crx)) //Hata
	//fmt.Printf("%T\n", (chan int)(crx))   //Hata
}

Çıktı:

c'nin türü:	chan int
ctx'in türü:	chan<- int
crx'in türü:	<-chan int
chan<- int
<-chan int

Bir Uygulama

ud-216

Yönlü kanalların anlamlı olacağı yer function signature olmaktadır. Bir fonksiyon send-only ya da receive-only bir kanalı parametre olarak alabilir. C’deki const benzetmesini hatırlayın. Orada da fonksiyon bildirimine bakarak bu fonksiyon bu pointer’ın içeriğini değiştirecek ya da değiştirmeyecek diyebiliyoruz. Burada da kanalın türüne bakarak fonksiyon sadece kanala veri koyabilir, alabilir ya da her ikisini de yapabilir diyoruz.

Aşağıdaki koda bakalım:

package main

import "fmt"

func main() {
	ch := make(chan int)

	go tx(ch)
	rx(ch)

	fmt.Println("Çıkış yapıyoruz.")
}

func tx(ch chan<- int) {
	ch <- 42
}

func rx(ch <-chan int) {
	fmt.Println(<-ch)
}

Burada func tx(ch chan<- int) ile tx() fonksiyonun sadece kanala gönderim yapacağını ve func rx(ch <-chan int) ile de rx() fonksiyonunun sadece alma yapacağını anlıyoruz. main() içerisinde de ch isminde bidirectional yani iki yönlü bir kanal yaratıyoruz. Bildiğimiz üzere function scope içerisindeki ch’lar aslında farklı, aynı isimde olması aynı olduğu anlamına gelmiyor. Yukarıda da gördüğümüz gibi ch -> chan<- T ve ch -> <-chan T dönüşümleri otomatik olarak (implicit casting) yapılıyor, o yüzden fonksiyonlara doğrudan argüman geçebiliyoruz.

Çıktı:

42
Çıkış yapıyoruz.

Dikkat ederseniz rx() fonksiyonunu bir goroutine olarak çağırmadık. Şimdi onu go diyerek goroutine haline getirerek çalıştıralım. Böyle yapınca çıktı

Çıkış yapıyoruz.

oldu.

Böyle olmak zorunda değildi, burada bir race condition var. Bunu önceden de görmüştük. main() sonlandığı anda programımız çıkıyor ve ch nin değerini bastıran fmt.Println() çalışmadan programımız bitebiliyor. Bitmemesini garanti etmek için önceden de kullandığımız gibi WaitGroup() kullanmamız lazımdı. Tabii kanallar ile de benzer bir senkronizasyon noktası oluşturabilirdik.

Burada eklemek istediğim bir nokta da kanal tutan değişkenlerin aslında slice gibi veri yapılarında olduğu gibi referans tutmasıdır. Yani biz iki adet fonksiyona ch değişkenini bir argüman olarak geçirsek de burada adeta C’deki pointer mantığı ile referans geçirildiği için tüm fonksiyonlar aynı kanal’ı görmektedir.