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.