Go'da select deyimi, birden çok kanal işleminden hangisinin gerçekleşeceğini seçer. Görünüşü switch'e benzer ama her case bir gönderme ya da alma işlemidir. Hazır olan işlemlerden biri rastgele seçilip yürütülür; hiçbiri hazır değilse select bekler, bir default dalı varsa beklemek yerine onu çalıştırır. Zaman aşımı için en kısa yol, bir case'e <-time.After(süre) eklemektir: işlem süresi içinde tamamlanmazsa zaman aşımı dalı seçilir.
Bu yazıda select'in spesifikasyondaki adım adım çalışma kuralını, default ile bloklamayan gönderme ve almayı, time.After, time.Timer ve time.Ticker ile zaman aşımı ve periyodik iş desenlerini, Go 1.23 ile değişen zamanlayıcı davranışını, nil kanalla case kapatmayı ve for içindeki break tuzağını Go 1.27.3 ile çalıştırılmış örneklerle anlatıyoruz. Kanal temelleri için Go channel yazısına, seri için Go öğrenme rehberi sayfasına bakabilirsiniz.
select adım adım nasıl çalışır?

Spesifikasyon select'in yürütülmesini beş adımda tanımlar. Bu adımları bilmek, ilk bakışta şaşırtıcı görünen davranışların çoğunu açıklar:
- 1. Değerlendirme:
select'e girilirken bütün case'lerdeki alma işlemlerinin kanal ifadeleri ile gönderme işlemlerinin kanal ve değer ifadeleri, kaynak koddaki sırayla tam bir kez değerlendirilir. Hangi case seçilirse seçilsin bu değerlendirmenin yan etkileri gerçekleşir. - 2. Seçim: İletişimlerden bir ya da daha fazlası gerçekleşebiliyorsa, bunlardan biri tekdüze sözde rastgele seçimle belirlenir. Hiçbiri gerçekleşemiyorsa ve
defaultvarsa o seçilir; yoksaselecten az biri gerçekleşebilene kadar bekler. - 3. İletişim: Seçilen case
defaultdeğilse ilgili gönderme ya da alma yürütülür. - 4. Atama: Seçilen case bir alma ise ve sol tarafta kısa değişken tanımı ya da atama varsa, sol taraf ifadeleri bu noktada değerlendirilir ve alınan değer atanır.
- 5. Gövde: Seçilen case'in deyim listesi çalışır.
switch'teki gibifallthroughyoktur.
İkinci adımın önemli bir sonucu vardır: birden çok kanal hazırsa yazdığınız sıra öncelik anlamına gelmez. Aşağıdaki programda her turda iki kanal da hazır; bin turun sonunda iki case'in de seçildiğini görüyoruz. Hangisinin kaç kez seçildiği her çalıştırmada değişir, bu yüzden yalnızca değişmeyen sonuçları yazdırıyoruz:
package main
import "fmt"
func main() {
a := make(chan int, 1)
b := make(chan int, 1)
sayac := map[string]int{}
for range 1000 {
a <- 1
b <- 1
select { // ikisi de hazır: seçim rastgele
case <-a:
sayac["a"]++
<-b
case <-b:
sayac["b"]++
<-a
}
}
fmt.Println("ikisi de seçildi:", sayac["a"] > 0 && sayac["b"] > 0)
fmt.Println("toplam:", sayac["a"]+sayac["b"])
}
// Çıktı:
// ikisi de seçildi: true
// toplam: 1000Bir kanala öncelik vermek istiyorsanız tek select yetmez; önce o kanalı default içeren ayrı bir select ile denemeniz, boşsa genel select'e geçmeniz gerekir.
default ile bloklamayan gönderme ve alma
Hiçbir case hazır değilken default dalı hemen çalıştığı için, default içeren bir select asla bloklanmaz. Bu, bir kanaldan "varsa al, yoksa devam et" ya da bir kanala "yer varsa gönder, yoksa bırak" demenin standart yoludur. Bir select'te en fazla bir default olabilir ve listede herhangi bir yerde durabilir:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
select { // bloklamayan alma
case v := <-ch:
fmt.Println("aldım:", v)
default:
fmt.Println("kanal boş, beklemeden geçtim")
}
ch <- 1
select { // bloklamayan gönderme
case ch <- 2:
fmt.Println("gönderdim")
default:
fmt.Println("tampon dolu, değer bırakıldı")
}
fmt.Println(<-ch)
}
// Çıktı:
// kanal boş, beklemeden geçtim
// tampon dolu, değer bırakıldı
// 1Effective Go bu tekniği "sızdıran tampon" (leaky buffer) örneğinde kullanır: istemci boş tampon listesinden bir tampon almayı dener, liste boşsa yenisini ayırır; sunucu işi bitince tamponu listeye geri koymayı dener, liste doluysa tamponu bırakır ve çöp toplayıcı onu temizler. Birkaç satırlık bu yapı, tamponlu kanal ve çöp toplayıcı sayesinde sınırlı boyutlu bir serbest liste oluşturur.
Case ifadeleri seçilmeden önce değerlendirilir
Birinci adım yüzünden, gönderme case'inde değeri hesaplayan bir fonksiyon çağırırsanız o fonksiyon case seçilmese bile çalışır. Aşağıdaki programda dolu kanalının alıcısı yok ve o case hiçbir zaman seçilemez, yine de deger fonksiyonu çağrılıyor:
package main
import "fmt"
func deger(ad string, n int) int {
fmt.Println("değerlendirildi:", ad)
return n
}
func main() {
dolu := make(chan int) // alıcısı yok, gönderme hiç gerçekleşmez
hazir := make(chan int, 1)
hazir <- 5
select {
case dolu <- deger("dolu", 1): // seçilmese de deger() çağrılır
fmt.Println("dolu'ya gönderildi")
case v := <-hazir:
fmt.Println("hazır'dan alındı:", v)
}
}
// Çıktı:
// değerlendirildi: dolu
// hazır'dan alındı: 5Pahalı bir hesaplamayı ya da yan etkisi olan bir çağrıyı case ifadesine koymayın; sonucu önce hesaplayıp bir değişkene atayın, select'te o değişkeni gönderin. Aynı kural case <-time.After(d) için de geçerlidir: select her çalıştığında yeni bir zamanlayıcı oluşturulur.
time.After ile zaman aşımı

Go Blog'daki "Timing out, moving on" yazısı zaman aşımını, işin sonucunu taşıyan kanal ile belirli süre sonra değer gönderen bir zamanlayıcı kanalını aynı select'te beklemek olarak anlatır. time.After(d), süre dolunca o anki zamanı gönderen bir kanal döndürür ve time.NewTimer(d).C ile eşdeğerdir. Aşağıdaki örnekte aynı fonksiyonu önce yetişen, sonra yetişemeyen bir işle çağırıyoruz:
package main
import (
"fmt"
"time"
)
func yavasIs(sure time.Duration) <-chan string {
ch := make(chan string, 1) // tampon: zaman aşımında goroutine takılı kalmasın
go func() {
time.Sleep(sure)
ch <- "sonuç hazır"
}()
return ch
}
func bekle(sure, limit time.Duration) {
select {
case s := <-yavasIs(sure):
fmt.Println(s)
case <-time.After(limit):
fmt.Println("zaman aşımı:", limit)
}
}
func main() {
bekle(10*time.Millisecond, 500*time.Millisecond)
bekle(500*time.Millisecond, 20*time.Millisecond)
}
// Çıktı:
// sonuç hazır
// zaman aşımı: 20msÖrnekteki yavasIs fonksiyonunun kanalı bir elemanlık tampon kullanıyor. Bu ayrıntı aynı blog yazısında da vurgulanır: zaman aşımı seçildiğinde artık kimse sonucu almayacaktır; kanal tamponsuz olsaydı işi yapan goroutine göndermeye çalışırken sonsuza kadar takılırdı. Tampon sayesinde goroutine değeri bırakıp çıkar, kanal da daha sonra çöp toplayıcı tarafından temizlenir. Aynı yazıdaki "ilk yanıt kazanır" örneği de, birden çok kopyaya aynı anda sorgu gönderip ilk geleni almak için tamponlu sonuç kanalı ve bloklamayan gönderme kullanır.
Go 1.23 öncesinde time.After belgesi, alttaki zamanlayıcının süresi dolana kadar çöp toplayıcı tarafından geri alınamayacağı konusunda uyarıyordu ve verimlilik önemliyse NewTimer ile Stop öneriliyordu. Güncel belgeye göre Go 1.23'ten itibaren çöp toplayıcı referansı kalmamış, durdurulmamış zamanlayıcıları da geri alabilir; time.After yeterli olduğunda NewTimer'ı tercih etmek için bir neden kalmamıştır.
for-select döngüsü, Ticker ve break tuzağı
Uzun süre çalışan goroutine'ler genellikle bir for döngüsü içinde select bekler: bir case iş kanalını, biri periyodik bir zamanlayıcıyı, biri de kapanma sinyalini dinler. Periyodik işler için time.NewTicker her periyotta zamanı gönderen bir Ticker döndürür. Belgeye göre ticker, yavaş alıcılara uyum sağlamak için aralığı ayarlayabilir ya da tik düşürebilir; süre sıfır ya da negatifse NewTicker panic eder.
Burada en sık yapılan hata break'in kapsamıdır. Spesifikasyona göre etiketsiz break, içinde bulunduğu en içteki for, switch ya da select deyimini sonlandırır. select içindeki bir break bu yüzden döngüden değil yalnızca select'ten çıkar ve döngü devam eder. Döngüden çıkmak için etiket kullanın ya da kodu bir fonksiyona alıp return edin:
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
bitir := time.After(110 * time.Millisecond)
tik := 0
dongu:
for {
select {
case <-ticker.C:
tik++
if tik == 3 {
fmt.Println("3 tik geldi, çıkıyorum")
break dongu // etiketsiz break yalnızca select'ten çıkardı
}
case <-bitir:
fmt.Println("süre doldu")
break dongu
}
}
fmt.Println("tik:", tik)
}
// Çıktı:
// 3 tik geldi, çıkıyorum
// tik: 3Ticker.Stop artık tik gönderilmemesini sağlar ama kanalı kapatmaz; böylece Reset çağrılabilir ve kanalı okuyan başka bir goroutine hatalı bir tik görmez. time.Tick ise yalnızca kanalı veren kısa bir sarmalayıcıdır ve süre sıfır ya da negatifse nil döndürür. Go 1.23 ile çöp toplayıcı referansı kalmamış ticker'ları durdurulmamış olsalar bile geri alabildiği için, belgeye göre Stop artık çöp toplayıcıya yardım etmek için gerekli değildir; tikleri gerçekten durdurmak istediğinizde çağırırsınız.
Timer: Stop, Reset ve Go 1.23 değişikliği
time.Timer tek seferlik bir olayı temsil eder. NewTimer(d) en az d süre sonra zamanı C kanalına gönderir. Stop zamanlayıcıyı durdurduysa true, zamanlayıcı zaten dolmuş ya da durdurulmuşsa false döndürür. Reset yeni bir süre kurar ve zamanlayıcı etkinse true, dolmuş ya da durdurulmuşsa false döndürür:
package main
import (
"fmt"
"time"
)
func main() {
t := time.NewTimer(time.Hour)
fmt.Println(len(t.C), cap(t.C)) // Go 1.23+: zamanlayıcı kanalı tamponsuz
fmt.Println("Stop:", t.Stop()) // çalışıyordu, durduruldu
fmt.Println("Stop:", t.Stop()) // zaten durmuştu
t.Reset(10 * time.Millisecond) // yeniden kur
<-t.C
fmt.Println("tetiklendi")
fmt.Println("Reset:", t.Reset(time.Hour)) // süresi dolmuştu: false
}
// Çıktı:
// 0 0
// Stop: true
// Stop: false
// tetiklendi
// Reset: falseGo 1.23 sürüm notları zamanlayıcılarda iki önemli değişiklik duyurur. Birincisi, artık referans verilmeyen Timer ve Ticker'lar Stop çağrılmamış olsa bile hemen çöp toplanabilir. İkincisi, zamanlayıcı kanalı artık tamponsuzdur (kapasite 0). Bunun sonucu olarak Reset ya da Stop döndükten sonra, çağrıdan önce hazırlanmış eski bir değer alınmaz. Eskiden kanal bir elemanlık tampon kullandığı için Stop false döndüğünde kanalı elle boşaltmak gerekiyordu. Görünür bir yan etki olarak timer kanallarında len ve cap artık 0 döner; yukarıdaki çıktının ilk satırı bunu gösteriyor.
Go 1.23'te bu yeni davranış yalnızca go.mod dosyasındaki go satırı 1.23.0 veya üstü olan programlarda etkindi ve asynctimerchan=1 GODEBUG ayarıyla eskiye dönülebiliyordu. Go 1.27 sürüm notlarına göre bu ayar kalıcı olarak kaldırıldı: time paketinin oluşturduğu kanallar artık GODEBUG ayarından bağımsız olarak her zaman tamponsuzdur. Eski bir kod tabanını güncellerken kanalı boşaltmaya yönelik eski if !t.Stop() { <-t.C } kalıplarını gözden geçirin; tamponsuz kanalda bu ek alma, değer gelmeyeceği için bloklanabilir.
time.AfterFunc(d, f) ise süre dolunca f'yi kendi goroutine'inde çağırır. Döndürdüğü Timer'ın C alanı kullanılmaz ve nil'dir; Stop ile çağrıyı iptal edebilirsiniz. Stop false döndürürse fonksiyon zaten başlamıştır ve Stop onun bitmesini beklemez:
package main
import (
"fmt"
"time"
)
func main() {
bitti := make(chan struct{})
t := time.AfterFunc(20*time.Millisecond, func() {
fmt.Println("AfterFunc kendi goroutine'inde çalıştı")
close(bitti)
})
fmt.Println("C alanı nil mi:", t.C == nil)
<-bitti
iptal := time.AfterFunc(time.Hour, func() { fmt.Println("hiç çalışmaz") })
fmt.Println("iptal edildi:", iptal.Stop())
}
// Çıktı:
// C alanı nil mi: true
// AfterFunc kendi goroutine'inde çalıştı
// iptal edildi: truenil kanal ile case'i devre dışı bırakmak
nil bir kanaldaki iletişim hiçbir zaman gerçekleşemez; select o case'i hazır olmayan bir case gibi yok sayar. Bu özellik, birden çok kaynağı kapanana kadar birleştirirken işi biten kaynağı devre dışı bırakmak için kullanılır. Kapanan kanalı nil yapmazsanız, kapalı kanaldan alma hep hemen tamamlandığı için o case durmadan seçilir ve döngü boşuna döner:
package main
import "fmt"
func kaynak(sayilar ...int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for _, n := range sayilar {
ch <- n
}
}()
return ch
}
func main() {
a, b := kaynak(1, 2, 3), kaynak(10, 20)
toplam := 0
for a != nil || b != nil {
select {
case v, ok := <-a:
if !ok {
a = nil // kapandı: bu case artık hiç seçilmez
continue
}
toplam += v
case v, ok := <-b:
if !ok {
b = nil
continue
}
toplam += v
}
}
fmt.Println("toplam:", toplam)
}
// Çıktı:
// toplam: 36Spesifikasyon bunun uç durumunu da tanımlar: yalnızca nil kanallardan oluşan ve default içermeyen bir select sonsuza kadar bekler. Hiç case içermeyen select {} de aynıdır. Programda bekleyen başka goroutine yoksa çalışma zamanı bunu kilitlenme olarak bildirir:
package main
func main() {
select {} // hiç case yok: sonsuza kadar bekler
}
// Çıktı (ilk satırlar):
// fatal error: all goroutines are asleep - deadlock!
//
// goroutine 1 [select (no cases)]:Desenler tek tabloda

| İhtiyaç | Desen | Not |
|---|---|---|
| Varsa al, yoksa geç | select + default | Asla bloklanmaz |
| En fazla d süre bekle | case <-time.After(d) | Sonuç kanalı tamponlu olsun |
| İstek boyunca zaman aşımı | context.WithTimeout + case <-ctx.Done() | Alt çağrılara yayılır |
| Periyodik iş | time.NewTicker + for-select | Çıkmak için etiketli break ya da return |
| Tek seferlik gecikmeli iş | time.AfterFunc | f kendi goroutine'inde çalışır |
| Biten kaynağı kapat | Kanal değişkenini nil yap | Kapalı kanal case'i aksi halde hep seçilir |
| Kapanma sinyali | case <-done (close ile) | Tüm bekleyenler aynı anda uyanır |
Zamana bağlı kodu test etmek
Zaman aşımı içeren kodu gerçek süreler bekleyerek test etmek yavaş ve kararsızdır. Go 1.25 ile genel kullanıma açılan testing/synctest paketi, testi yalıtılmış bir "kabarcık" içinde çalıştırır; kabarcıktaki goroutine'ler sahte bir saat kullanır ve synctest.Wait kabarcıktaki bütün goroutine'ler bloklanana kadar bekler. Go 1.27 sürüm notlarına göre pakete time.Sleep ile synctest.Wait'i birleştiren Sleep yardımcısı eklendi. Test yazımının genel kuralları için Go test yazma yazısına bakabilirsiniz.
Sık yapılan hatalar
- select içindeki break'in döngüden çıkaracağını sanmak: Yalnızca
select'ten çıkar; etiket ya dareturnkullanın. - Case sırasını öncelik sanmak: Birden çok case hazırsa seçim rastgeledir.
- Zaman aşımında sonuç kanalını tamponsuz bırakmak: İşi yapan goroutine sonucu gönderemeden takılı kalır ve sızar.
- for içinde default ile meşgul bekleme: İşlemci boşuna yanar; beklemek istiyorsanız
defaultkullanmayın. - Case ifadesinde yan etkili çağrı: İfade, case seçilmese de değerlendirilir.
- Kapalı kanalın case'ini açık bırakmak: Kapalı kanaldan alma hep hazır olduğu için o case sürekli seçilir; kanalı nil yapın.
- Go 1.23 öncesine ait timer boşaltma kalıpları: Tamponsuz timer kanalında fazladan
<-t.Cbloklanabilir. - Sıfır ya da negatif süreyle NewTicker: Panic eder;
time.Tickise bu durumda nil kanal döndürür ve onu bekleyen case hiç seçilmez.
Sık Sorulan Sorular
select ile switch arasındaki fark nedir?
switch değerleri karşılaştırır ve case'leri yukarıdan aşağıya dener. select'te her case bir kanal işlemidir; hazır olanlar arasında rastgele seçim yapılır ve hiçbiri hazır değilse bekler. select'te fallthrough yoktur.
time.After mı, time.NewTimer mı kullanmalıyım?
Tek seferlik bir zaman aşımı için time.After yeterlidir; Go 1.23'ten beri belge NewTimer'ı tercih etmek için bir neden olmadığını söyler. Zamanlayıcıyı durdurmanız, yeniden kurmanız ya da birden çok kez kullanmanız gerekiyorsa NewTimer ile Stop ve Reset kullanın.
select hiç case içermezse ne olur?
select {} sonsuza kadar bekler. Başka goroutine'ler çalışmaya devam ediyorsa program açık kalır; bekleyen başka goroutine kalmazsa çalışma zamanı all goroutines are asleep - deadlock! hatasıyla programı sonlandırır.
Bir kanala öncelik verebilir miyim?
Tek bir select ile hayır. Öncelikli kanalı önce default içeren bir select ile kontrol edip, değer yoksa diğer kanalları da içeren ikinci bir select'e geçerek öncelik kurabilirsiniz.
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 channel (kanallar) · Go goroutine · context paketi · time paketi: tarih ve saat · Go test yazma · sync.Mutex ve WaitGroup