Go'da kanal (channel), goroutine'lerin belirli bir tipteki değerleri birbirine göndermesini ve almasını sağlayan tipli bir iletişim borusudur. make(chan int) tamponsuz, make(chan int, 10) on elemanlık tamponlu bir kanal oluşturur. Gönderme ch <- v, alma v := <-ch biçimindedir. Tamponsuz kanalda gönderen ve alan birbirini bekler; tamponlu kanalda gönderen yalnızca tampon doluysa bekler. Gönderecek değer kalmadığında gönderen taraf close(ch) ile kanalı kapatır, alıcı da for v := range ch döngüsüyle kanal kapanana kadar okur.
Bu yazıda kanal oluşturmayı, tamponsuz ve tamponlu kanal farkını, close, range ve v, ok biçimini, yönlü kanalları, nil kanalı, kanal işlemlerinin hangi durumda panic ya da kilitlenme (deadlock) ürettiğini ve boru hattı, semafor, istek-yanıt gibi yaygın desenleri Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Goroutine temelleri için goroutine yazısına, tüm seri için Go öğrenme rehberi sayfasına bakabilirsiniz.
Kanal nedir ve nasıl oluşturulur?

Spesifikasyona göre kanal, eşzamanlı çalışan fonksiyonların belirli bir eleman tipindeki değerleri göndererek ve alarak iletişim kurmasını sağlayan bir mekanizmadır. Kanallar da map'ler gibi make ile oluşturulur ve ortaya çıkan değer arka plandaki bir veri yapısına referans gibi davranır. Bir kanalı fonksiyona geçirdiğinizde kopyalanan şey bu referanstır; iki taraf da aynı kanalı kullanır. İlk değer atanmamış bir kanal değişkeninin değeri nil'dir.
Tek bir kanal, gönderme ve alma işlemlerinde ve len, cap çağrılarında, ek bir senkronizasyon olmadan istenen sayıda goroutine tarafından kullanılabilir. Kanallar ilk giren ilk çıkar (FIFO) kuyruğu gibi davranır: bir goroutine değerleri sırayla gönderiyor, bir başkası alıyorsa değerler gönderildiği sırayla alınır.
Effective Go bu yaklaşımı bir sloganla özetler: belleği paylaşarak iletişim kurmayın, iletişim kurarak belleği paylaşın. Bir değer kanalla bir goroutine'den diğerine geçtiğinde, herhangi bir anda o değere yalnızca bir goroutine erişir. Aynı belge bu fikrin abartılabileceğini de söyler: örneğin bir referans sayacını korumak için tamsayının etrafına bir mutex koymak daha uygun olabilir. Kanal mı kilit mi kararını sync paketi yazısında ayrıca ele alıyoruz.
Tamponsuz kanal: buluşma noktası
Kapasite verilmezse ya da 0 verilirse kanal tamponsuzdur. Tamponsuz kanalda iletişim ancak gönderen ve alan aynı anda hazır olduğunda gerçekleşir. Gönderen, alıcı değeri alana kadar; alıcı da bir değer gelene kadar bekler. Effective Go bunu "iletişimi senkronizasyonla birleştirmek" olarak tarif eder: değer el değiştirdiği anda iki goroutine'in de bilinen bir noktada olduğu garanti edilir.
package main
import "fmt"
func main() {
ch := make(chan string) // tamponsuz (kapasite 0)
go func() {
ch <- "ping" // alıcı hazır olana kadar bekler
}()
mesaj := <-ch // gönderici gelene kadar bekler
fmt.Println(mesaj)
fmt.Println(len(ch), cap(ch))
}
// Çıktı:
// ping
// 0 0Bellek modeli açısından bir kanaldaki gönderme, karşılık gelen almanın tamamlanmasından önce eşitlenir. Tamponsuz kanalda bunun tersi de geçerlidir: alma, karşılık gelen göndermenin tamamlanmasından önce eşitlenir. Bu yüzden tamponsuz bir kanaldan sinyal aldığınızda, gönderenin o ana kadar yaptığı yazmaları güvenle okuyabilirsiniz.
Tamponlu kanal: len ve cap
make'e ikinci argüman olarak pozitif bir kapasite verirseniz kanal tamponlu olur. Tamponda yer varsa gönderme beklemeden tamamlanır, tampon boş değilse alma beklemeden tamamlanır. len(ch) tamponda bekleyen eleman sayısını, cap(ch) kapasiteyi verir:
package main
import "fmt"
func main() {
ch := make(chan int, 3) // 3 elemanlık tampon
ch <- 10 // alıcı yok ama tamponda yer var: beklemez
ch <- 20
fmt.Println("len:", len(ch), "cap:", cap(ch))
fmt.Println(<-ch) // FIFO: ilk giren ilk çıkar
fmt.Println("len:", len(ch))
fmt.Println(<-ch)
}
// Çıktı:
// len: 2 cap: 3
// 10
// len: 1
// 20len değeri yalnızca o anın görüntüsüdür; başka goroutine'ler aynı anda gönderip alıyorsa bir sonraki satırda değişmiş olabilir. Bir alma işleminin bloklanıp bloklanmayacağına len'e bakarak karar vermek yerine select ile bloklamayan alma kullanın. Go 1.23 sürüm notları da zamanlayıcı kanallarının kapasitesi 0'a indiğinde len sorgulayan kodların etkilendiğini ve bu tür kodun bloklamayan alma kullanması gerektiğini belirtir; konu select ve zaman aşımı yazısında ayrıntılı anlatılıyor.
Tampon boyutu bir performans ayarı gibi görünse de aslında programın davranışını değiştirir. Go Blog'daki pipelines yazısı, takılan goroutine'leri çözmek için tampon boyutunu "tam gerektiği kadar" seçmenin kırılgan olduğunu gösterir: gönderilen değer sayısı ya da alıcının tükettiği değer sayısı değişirse goroutine'ler yeniden takılır. Tamponu, gönderilecek değer sayısı kanal oluşturulurken kesin olarak biliniyorsa ya da bilinçli bir sınır (semafor) koymak istiyorsanız kullanın.
close, range ve v, ok biçimi
close(ch), kanala artık değer gönderilmeyeceğini kaydeder. Kapatmadan önce gönderilmiş değerler alınmaya devam eder; tampon boşaldıktan sonra her alma işlemi beklemeden kanal eleman tipinin sıfır değerini döndürür. Çok değerli alma biçimi v, ok := <-ch ile alınan değerin gerçek bir göndermeden mi geldiğini (ok == true) yoksa kanal kapalı ve boş olduğu için üretilen sıfır değer mi olduğunu (ok == false) ayırt edersiniz. for v := range ch döngüsü ise kanal kapanana kadar gelen ardışık değerleri sırayla verir:
package main
import "fmt"
func uret(n int) <-chan int {
out := make(chan int)
go func() {
defer close(out) // gönderici kapatır
for i := 1; i <= n; i++ {
out <- i
}
}()
return out
}
func main() {
for v := range uret(3) { // kanal kapanınca döngü biter
fmt.Println("aldım:", v)
}
ch := make(chan int, 1)
ch <- 7
close(ch)
v, ok := <-ch
fmt.Println(v, ok) // kapalı ama tamponda değer vardı
v, ok = <-ch
fmt.Println(v, ok) // kapalı ve boş: sıfır değer, false
}
// Çıktı:
// aldım: 1
// aldım: 2
// aldım: 3
// 7 true
// 0 falseTour of Go iki önemli not düşer. Birincisi, kanalı yalnızca gönderen taraf kapatmalıdır, alıcı asla kapatmamalıdır; çünkü kapalı kanala gönderme panic üretir ve alıcı, göndericinin işinin bitip bitmediğini bilemez. İkincisi, kanallar dosya gibi değildir; genellikle kapatmanız gerekmez. Kapatmak yalnızca alıcıya artık değer gelmeyeceğini söylemek gerektiğinde, örneğin bir range döngüsünü bitirmek için gereklidir. Kullanılmayan kanalı çöp toplayıcı temizler.
Kapatma bir yayın (broadcast) sinyali olarak da kullanılır. Kapalı bir kanaldan alma her zaman hemen tamamlandığı için, aynı kanalı bekleyen sınırsız sayıda goroutine tek bir close ile aynı anda uyandırılabilir. Go Blog'daki pipelines yazısı iptal sinyalini bu yolla verir; context.Context'in Done kanalı da aynı fikre dayanır. Bellek modeline göre kanalın kapatılması, kapalı olduğu için sıfır değer döndüren almadan önce eşitlenir.
Kanal işlemleri hangi durumda ne yapar?

Spesifikasyondaki kuralları tek tabloda toplamak hataları önlemenin en kısa yoludur. Satırlar kanalın durumunu, sütunlar işlemleri gösterir:
| Durum | Gönderme ch <- v | Alma <-ch | close(ch) |
|---|---|---|---|
| nil kanal | Sonsuza kadar bekler | Sonsuza kadar bekler | panic: close of nil channel |
| Açık, tamponsuz | Alıcı hazır olana kadar bekler | Gönderen gelene kadar bekler | Kapatır |
| Açık, tamponlu | Tampon doluysa bekler | Tampon boşsa bekler | Kapatır |
| Kapalı | panic: send on closed channel | Önce kalan değerler, sonra hemen sıfır değer ve ok false | panic: close of closed channel |
| Yalnızca alınabilen <-chan T | Derleme hatası | Normal | Derleme hatası |
| Yalnızca gönderilebilen chan<- T | Normal | Derleme hatası | Normal |
Kapalı kanala göndermenin ilk satırı şöyledir; çıktının devamında yığın izi gelir:
package main
func main() {
ch := make(chan int, 1)
close(ch)
ch <- 1
}
// Çıktı (ilk satırlar):
// panic: send on closed channelAynı kanalı iki kez kapatmak ve nil bir kanalı kapatmak da çalışma zamanında panic üretir:
package main
func main() {
ch := make(chan int)
close(ch)
close(ch)
}
// Çıktı (ilk satırlar):
// panic: close of closed channelpackage main
func main() {
var ch chan int
close(ch)
}
// Çıktı (ilk satırlar):
// panic: close of nil channelKanal kullanan programlarda en sık görülen çalışma zamanı hatası ise kilitlenmedir. Tamponsuz bir kanala, alıcısı olmayan tek goroutine'den göndermek main'i sonsuza kadar bekletir. Çalışma zamanı bütün goroutine'lerin uyuduğunu fark ettiğinde programı ölümcül hatayla sonlandırır ve hangi goroutine'in hangi işlemde beklediğini yazar:
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1 // alıcı yok: main sonsuza kadar bekler
fmt.Println(<-ch)
}
// Çıktı (ilk satırlar):
// fatal error: all goroutines are asleep - deadlock!
//
// goroutine 1 [chan send]:Bu hata yalnızca bütün goroutine'ler beklediğinde tetiklenir. Programda çalışan başka bir goroutine varsa (örneğin bir HTTP sunucusu) takılı kalan goroutine sessizce bekler ve bir sızıntıya dönüşür. Hata mesajlarının listesi ve çözümleri için Go sık karşılaşılan hatalar yazısına bakabilirsiniz.
nil kanal
make ile oluşturulmamış bir kanal nil'dir. Spesifikasyona göre nil kanal hiçbir zaman iletişime hazır olmaz: nil kanala gönderme de ondan alma da sonsuza kadar bekler, range ile nil kanal üzerinde dönmek de sonsuza kadar bloklanır. len ve cap nil kanal için 0 döndürür. Çalışma zamanı, kilitlenme mesajında bu durumu ayrıca belirtir:
package main
import "fmt"
func main() {
var ch chan int // make yok: nil kanal
fmt.Println(ch == nil, len(ch), cap(ch))
<-ch // nil kanaldan almak sonsuza kadar bekler
}
// Çıktı (ilk satırlar):
// true 0 0
// fatal error: all goroutines are asleep - deadlock!
//
// goroutine 1 [chan receive (nil chan)]:Bu özellik ilk bakışta tuzak gibi görünse de select içinde kullanışlıdır: işi biten bir kanal değişkenini nil yaparak o case'i devre dışı bırakabilirsiniz. Bu deseni select yazısında çalışan bir örnekle gösteriyoruz.
Yönlü kanallar: chan<- ve <-chan
Kanal tipine yön ekleyerek bir fonksiyonun kanalı yalnızca gönderme ya da yalnızca alma için kullanmasını zorunlu kılabilirsiniz. chan<- float64 yalnızca gönderilebilen, <-chan int yalnızca alınabilen bir kanaldır. Çift yönlü bir kanal atama ya da açık dönüşümle tek yönlü kanala dönüştürülebilir; tersi mümkün değildir. <- işleci mümkün olan en soldaki chan ile birleşir, yani chan<- chan int ifadesi chan<- (chan int) ile aynıdır.
Yön kısıtı derleme zamanında denetlenir. Aşağıdaki programda yalnızca gönderilebilen bir kanaldan okumaya çalışıyoruz:
package main
func uret(out chan<- int) {
out <- 1
v := <-out // yalnızca gönderilebilen kanaldan okuma
_ = v
}
func main() {
ch := make(chan int, 1)
uret(ch)
}# command-line-arguments
./main.go:5:9: invalid operation: cannot receive from send-only channel chan<- int out (variable of type chan<- int)Yalnızca alınabilen bir kanalı close ile kapatmak da derleme hatasıdır; spesifikasyon bunu açıkça yasaklar. Bu kural, "kanalı gönderen kapatır" ilkesini tip sistemiyle destekler: bir fonksiyon <-chan T döndürüyorsa kanalı kapatma sorumluluğu o fonksiyonun içindeki göndericidedir. Yukarıdaki uret örneğinde de dışarıya yalnızca alınabilen kanal verdik.
Boru hattı (pipeline) deseni
Go Blog'daki pipelines yazısı boru hattını, kanallarla birbirine bağlanmış aşamalar dizisi olarak tanımlar. Her aşama yukarıdan gelen değerleri alır, üzerinde bir işlem yapar ve sonucu aşağıya gönderir. Aşağıdaki örnekte gen sayıları kanala döker, kare her sayının karesini alır. Giriş ve çıkış tipleri aynı olduğu için kare'yi iki kez zincirleyebiliyoruz:
package main
import "fmt"
func gen(sayilar ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range sayilar {
out <- n
}
close(out)
}()
return out
}
func kare(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// gen -> kare -> kare: aşamalar aynı tipte olduğu için zincirlenebilir
for v := range kare(kare(gen(1, 2, 3))) {
fmt.Println(v)
}
}
// Çıktı:
// 1
// 16
// 81Yazı iki kural önerir: her aşama tüm göndermeleri bitirince çıkış kanalını kapatır ve her aşama giriş kanalı kapanana kadar almaya devam eder. Böylece her alıcı aşama bir range döngüsü olarak yazılabilir. Aynı kanaldan birden çok fonksiyonun okumasına fan-out, birden çok kanalın tek kanalda birleştirilmesine fan-in denir; birleştirme yapan fonksiyon çıkış kanalını, bütün gönderimler bittikten sonra kapatmak için genellikle sync.WaitGroup kullanır.
Gerçek boru hatlarında alıcı her zaman tüm değerleri tüketmez; erken çıkan bir alıcı, yukarıdaki aşamaların göndermeye çalışırken takılmasına neden olur. Çözüm, alıcının vazgeçtiğini göndericilere bildirmektir. Bunun için bir done kanalı kapatılır ve göndericiler göndermeyi select içinde done ile birlikte bekler. Bugün bu iş için genellikle context paketi kullanılır.
Tamponlu kanal ile semafor
Tamponlu bir kanal sayaçlı semafor olarak kullanılabilir. Bellek modeli bunu açıkça tanımlar: kapasitesi C olan bir kanaldaki k'ıncı alma, k+C'inci göndermenin tamamlanmasından önce eşitlenir. Kanaldaki eleman sayısı o anki kullanım sayısına, kapasite en fazla eşzamanlı kullanıma karşılık gelir; göndermek semaforu almak, almak semaforu bırakmaktır. Aşağıdaki programda altı iş başlatıyoruz ama aynı anda en fazla iki tanesinin çalışmasına izin veriyoruz:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
sem := make(chan struct{}, 2) // aynı anda en fazla 2 iş
var mu sync.Mutex
aktif, enFazla := 0, 0
var wg sync.WaitGroup
for range 6 {
wg.Go(func() {
sem <- struct{}{} // yer ayır (doluysa bekle)
defer func() { <-sem }() // yeri bırak
mu.Lock()
aktif++
enFazla = max(enFazla, aktif)
mu.Unlock()
time.Sleep(10 * time.Millisecond) // işi temsil eder
mu.Lock()
aktif--
mu.Unlock()
})
}
wg.Wait()
fmt.Println("sınır aşılmadı:", enFazla <= 2)
}
// Çıktı:
// sınır aşılmadı: trueBoş yapı tipi struct{} bellek kaplamadığı için yalnızca sinyal taşıyan kanallarda eleman tipi olarak sık kullanılır. Effective Go, her istek için goroutine başlatıp semaforla sınırlayan bir sunucunun yine de sınırsız goroutine oluşturabileceğini hatırlatır; bu durumda semaforu goroutine başlatmadan önce almak ya da sabit sayıda işçi kullanmak gerekir.
Kanal üzerinden kanal: istek ve yanıt
Kanallar birinci sınıf değerlerdir; bir yapı alanında saklanabilir, başka bir kanal üzerinden gönderilebilir. Effective Go bunu, her istemcinin yanıtını alacağı kanalı isteğin içine koyduğu bir desenle gösterir. Sunucu tarafı yalnızca isteği işleyip sonucu istemcinin kanalına yazar:
package main
import "fmt"
type istek struct {
sayilar []int
yanit chan int // her istemci kendi yanıt kanalını getirir
}
func sunucu(kuyruk <-chan istek) {
for i := range kuyruk {
toplam := 0
for _, n := range i.sayilar {
toplam += n
}
i.yanit <- toplam
}
}
func main() {
kuyruk := make(chan istek)
go sunucu(kuyruk)
i := istek{sayilar: []int{3, 4, 5}, yanit: make(chan int)}
kuyruk <- i
fmt.Println("toplam:", <-i.yanit)
close(kuyruk)
}
// Çıktı:
// toplam: 12Belge bu iskeletin, ortada tek bir mutex olmadan hız sınırlamalı, paralel ve bloklamayan bir RPC sisteminin temelini oluşturduğunu söyler. Gerçek bir sistemde zaman aşımı ve iptal de gerekir; bunlar için select ve context yazılarına geçebilirsiniz.
Sık yapılan hatalar

- Alıcı tarafta close çağırmak: Gönderen hâlâ çalışıyorsa kapalı kanala gönderip panic üretir. Kanalı gönderen kapatsın; birden çok gönderen varsa hepsi bittikten sonra tek bir yerden kapatın.
- Kanalı iki kez kapatmak:
close of closed channelpanic'i. Kapatmayı tek bir sahibe verin. - Kanal kapanmadan range ile okumak: Gönderen kanalı kapatmazsa
rangedöngüsü hiç bitmez. - Tamponsuz kanala aynı goroutine'den gönderip almak: Alıcı aynı goroutine'de sırada beklediği için kilitlenir.
- v, ok kullanmadan sıfır değere güvenmek: Kapalı kanaldan gelen 0 ile gönderilmiş gerçek 0'ı ayırt edemezsiniz.
- Tampon boyutunu kilitlenmeyi gizlemek için büyütmek: Sorunu yalnızca erteler; alıcı-gönderici sayısını ya da iptali düzeltin.
- Gönderme ile close'u senkronize etmemek: Race detector belgesine göre aynı kanaldaki eşitlenmemiş gönderme ve kapatma da veri yarışıdır.
Sık Sorulan Sorular
Bir kanalın kapalı olup olmadığını nasıl anlarım?
Kanalın durumunu sorgulayan bir fonksiyon yoktur. Alma işlemini v, ok := <-ch biçiminde yapar ve ok değerine bakarsınız; false ise kanal kapalı ve boştur. Bloklanmadan denemek istiyorsanız bu almayı default içeren bir select içine koyun.
Her kanalı kapatmak zorunda mıyım?
Hayır. Tour of Go'nun belirttiği gibi kanallar dosya değildir. Kapatma yalnızca alıcıya başka değer gelmeyeceğini bildirmek için gerekir; örneğin range döngüsünü bitirmek ya da birden çok goroutine'e yayın sinyali vermek için.
Tamponsuz mu tamponlu mu kullanmalıyım?
Varsayılan olarak tamponsuz kanalla başlayın; davranışı en kolay akıl yürütülen kanal odur ve gönderme ile alma arasında güçlü bir senkronizasyon sağlar. Gönderilecek değer sayısı belliyse, göndericilerin alıcıyı beklemeden çıkması gerekiyorsa ya da eşzamanlılığı sınırlamak istiyorsanız tamponlu kanal kullanın.
Kanal mı, mutex mi?
Veri sahipliği bir goroutine'den diğerine geçiyorsa, iş dağıtıyorsanız ya da olayları bildiriyorsanız kanal doğal seçimdir. Birden çok goroutine'in aynı yapının iç durumunu kısa süreli okuyup güncellediği durumlarda, örneğin bir sayaç ya da önbellek, sync.Mutex genellikle daha basittir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Go öğrenme rehberi · Go goroutine · select ve zaman aşımı · sync.Mutex ve WaitGroup · context paketi · range over func ve iteratorler · Go sık karşılaşılan hatalar