Program concurrent jarang hanya menunggu satu kejadian: server menunggu permintaan baru sekaligus sinyal berhenti, pekerja menunggu tugas berikutnya sekaligus batas waktu. Statement blocking biasa hanya bisa menghadapi satu channel pada satu waktu, sehingga menunggu yang kedua harus menunggu selesainya yang pertama — urutan seperti itu mudah deadlock. Go menyediakan select, statement pembanding banyak operasi channel sekaligus yang menunggu hingga satu operasi siap lalu menjalankan cabangnya. Chapter ini membahas select inti, default, pola timeout dengan time.After, serta kejujuran pemilihan acaknya.
Menunggu Banyak Channel Sekaligus #
Bentuk select mirip switch, tetapi setiap case memuat operasi channel — kirim atau terima — bukan perbandingan nilai:
select {
case v := <-chA:
fmt.Println("dari chA:", v)
case v := <-chB:
fmt.Println("dari chB:", v)
}Runtime menunggu semua operasi case sekaligus. Ketika tepat satu operasi siap, select menjalankan cabangnya. Ketika beberapa operasi siap bersamaan, Go memilih secara acak semu (uniform pseudo-random) — kejujuran ini mencegah satu channel yang sibuk membuat channel lain kelaparan, tetapi juga berarti kita tidak boleh mengandalkan urutan pemilihan. Berkas select-demo.go mempraktikkan tiga wajah select dalam satu program:
package main
import (
"fmt"
"time"
)
func main() {
// bagian 1: hanya satu channel yang siap
siap := make(chan string)
lainnya := make(chan string)
go func() {
siap <- "satu-satunya yang siap"
}()
select {
case m := <-siap:
fmt.Println("diterima dari siap :", m)
case m := <-lainnya:
fmt.Println("diterima dari lainnya :", m)
}
// bagian 2: default membuat operasi non-blocking
ch := make(chan int)
select {
case ch <- 99:
fmt.Println("terkirim")
default:
fmt.Println("tidak ada penerima, kirim ditunda")
}
// bagian 3: timeout dengan time.After
lambat := make(chan string)
select {
case m := <-lambat:
fmt.Println("diterima dari lambat :", m)
case <-time.After(50 * time.Millisecond):
fmt.Println("timeout: 50 ms tanpa data")
}
}Output yang Dihasilkan:
diterima dari siap : satu-satunya yang siap
tidak ada penerima, kirim ditunda
timeout: 50 ms tanpa dataMari kita telaah baris demi baris program di atas:
- Baris 13–15 (
go func): satu goroutine mengirim ke channelsiap; channellainnyatidak pernah dikirimi apa pun sepanjang program. - Baris 17–22 (
selectpertama): karena hanyacase <-siapyang siap, cabang itu pasti terpilih — hasil deterministik; pemilihan acak hanya berlaku bila banyak case siap bersamaan. - Baris 25–31 (
selectkedua):chunbuffered tanpa penerima, jadi tidak ada case siap;defaultlangsung dijalankan dan kirim ditinggalkan — operasi berubah non-blocking. - Baris 34–40 (
selectketiga):time.After(50 * time.Millisecond)mengembalikan channel yang mengirim nilai tepat setelah 50 milidetik; channellambattidak pernah siap, sehingga satu-satunya case yang siap adalah timeout.
default: Operasi Non-Blocking #
Tanpa default, select menunggu sampai ada case yang siap. Dengan default, select menjadi percobaan sekali putus: bila tidak ada case siap, cabang default dijalankan tanpa menunggu sama sekali. Pola ini berguna untuk mengirim “bila ada yang mau” — misalnya membuang event yang tidak sempat diproses — atau memeriksa antrean tanpa memblokir thread.
Timeout dengan time.After #
Kombinasi case <-time.After(d) memberi batas waktu pada penantian: jika data tidak datang dalam durasi d, cabang timeout menang dan program bisa mengambil keputusan — mencoba ulang, melanjutkan tanpa data, atau membatalkan. Pada program di atas timeout menang karena channel lambat tidak pernah menerima kiriman. Perhatikan bahwa setiap evaluasi time.After membuat timer baru; di dalam loop yang panjang, pola pembatalan yang lebih hemat dan lebih terstruktur adalah context yang dibahas di chapter Cancellation dan Timeout
.
select Lainnya yang Perlu Diketahui #
select tanpa case apa pun (select {}) memblokir goroutine selamanya — bentuk ini hampir selalu muncul sebagai bug, dan runtime melaporkannya sebagai deadlock bila tidak ada goroutine lain yang bisa maju. Sebaliknya, case yang memakai channel nil tidak pernah siap, sehingga mengisi channel dengan nil adalah cara mematikan case tertentu tanpa mengubah struktur select. Terakhir, select di dalam for tanpa break membentuk loop event — pola inti worker yang menunggu tugas: loop menangani satu event per iterasi, dan kombinasi label + break keluar dari loop.
Perbedaan mendasar dengan polling perlu ditegaskan. Polling bertanya berkali-kali “sudah siap belum?” sambil menghabiskan CPU; select mendaftarkan sekali “panggil saya bila siap” lalu tidur tanpa beban hingga event tiba. Untuk menunggu banyak goroutine selesai bekerja — bukan menunggu kiriman data — Go punya alat khusus yang lebih tepat: sync.WaitGroup pada chapter berikutnya.
Referensi #
- Select statements — Spesifikasi Go — aturan resmi pemilihan case, default, dan acak.
- Package time
— dokumentasi
time.Afterdan timer. - Go Concurrency Patterns: Pipelines — The Go Blog — pemakaian select pada pipeline.
Dengan select, goroutine bisa menunggu banyak kejadian sekaligus secara adil. Bab berikutnya menutup masalah lain: menunggu sekelompok goroutine tuntas tanpa memusingkan channel konfirmasi satu per satu.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
select-demo.go
Unduh berkaspackage main
import (
"fmt"
"time"
)
func main() {
// bagian 1: hanya satu channel yang siap
siap := make(chan string)
lainnya := make(chan string)
go func() {
siap <- "satu-satunya yang siap"
}()
select {
case m := <-siap:
fmt.Println("diterima dari siap :", m)
case m := <-lainnya:
fmt.Println("diterima dari lainnya :", m)
}
// bagian 2: default membuat operasi non-blocking
ch := make(chan int)
select {
case ch <- 99:
fmt.Println("terkirim")
default:
fmt.Println("tidak ada penerima, kirim ditunda")
}
// bagian 3: timeout dengan time.After
lambat := make(chan string)
select {
case m := <-lambat:
fmt.Println("diterima dari lambat :", m)
case <-time.After(50 * time.Millisecond):
fmt.Println("timeout: 50 ms tanpa data")
}
}