Menu

Pernyataan select di Golang: Timeout, default, dan Quit Channel

Cara select menunggu beberapa operasi channel sekaligus: memilih di antara case yang siap, send dan receive non-blocking dengan default, timeout dengan time.After, dan menghentikan perulangan dengan quit channel atau context.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

select terlihat seperti switch, tapi setiap case-nya adalah send atau receive pada channel. Ia terblokir sampai salah satu case bisa berjalan, lalu menjalankan case itu.

Dengan jeda waktu seperti ini, select pertama mendapat hasil yang cepat dan yang kedua mendapat hasil yang lambat. Tidak ada receive yang harus menunggu channel lainnya. <-slow biasa yang diikuti <-fast akan menangani keduanya dalam urutan tetap, tidak peduli mana yang tiba lebih dulu.

Cara select dievaluasi:

  1. Semua ekspresi channel dan nilai yang akan dikirim dievaluasi satu kali, sesuai urutan di source code, saat select dimulai.
  2. Kalau satu atau lebih case siap, salah satunya dipilih secara acak.
  3. Kalau tidak ada yang siap dan ada default, default yang dijalankan.
  4. Kalau tidak, goroutine terblokir sampai ada case yang siap.

select {} yang kosong terblokir selamanya. Sesekali kamu akan melihatnya di akhir main pada program yang pekerjaan sebenarnya terjadi di goroutine lain.

Pilihan acak di antara case yang siap

Saat beberapa case siap pada saat yang sama, select tidak mengutamakan case yang ditulis pertama. Program ini mengisi dua buffered channel lalu melakukan select 1000 kali:

Pembagian antara countA dan countB berubah setiap kali dijalankan dan berada di sekitar 500 untuk masing-masing. Pilihan acak ini disengaja: mencegah channel yang sibuk membuat channel lain kelaparan. Kalau kamu butuh prioritas, lihat polanya di bagian bawah.

Operasi non-blocking dengan default

Dengan case default, select tidak pernah terblokir. Ini mengubah send atau receive menjadi operasi "coba dulu":

Membuang pekerjaan saat buffer penuh adalah cara mengurangi beban atau mengirim metrik tanpa pernah membuat pemanggil tertahan.

Jangan menaruh default di select di dalam loop for hanya untuk "memeriksa" channel berulang-ulang. Kalau tidak ada yang siap, perulangan itu berputar dengan CPU 100%. Lebih baik biarkan terblokir, dan tambahkan case timeout kalau kamu perlu bangun secara berkala.

Timeout

time.After(d) mengembalikan channel yang menerima satu kali setelah d. Adu channel itu dengan pekerjaan yang sebenarnya:

Pemanggilan pertama mengembalikan "data". Pemanggilan kedua mengembalikan error timeout setelah 50 ms, jauh sebelum worker selesai. Channel hasil sengaja diberi buffer berukuran 1. Saat timeout yang menang, tidak ada yang pernah menerima dari result; dengan unbuffered channel, goroutine worker akan terblokir selamanya saat mengirim dan bocor.

Di dalam perulangan, time.After membuat timer baru di setiap iterasi, dan itu tepat untuk timeout idle per pesan ("tidak ada pesan selama 1 detik"). Untuk deadline keseluruhan yang mencakup banyak operasi, buat satu timer atau context sebelum perulangan. Sejak Go 1.23, timer yang sudah tidak dirujuk lagi akan dibersihkan garbage collector walaupun belum berbunyi, jadi time.After di dalam perulangan tidak lagi menahan memori sampai setiap timer berbunyi seperti di versi lama (ini butuh go 1.23 atau lebih baru di go.mod).

Loop for-select dan quit channel

Goroutine yang berjalan sampai disuruh berhenti adalah loop for yang membungkus select dengan satu case untuk pekerjaan dan satu case untuk berhenti:

Menutup quit alih-alih mengirim ke dalamnya adalah idiomnya: penutupan terlihat oleh setiap penerima, sekarang maupun nanti, jadi satu close bisa menghentikan worker sebanyak apa pun. Channel done membuat main bisa menunggu sampai worker benar-benar selesai.

Di kode sungguhan, quit channel biasanya berupa context.Context: case <-ctx.Done():. Cara kerjanya sama (Done() mengembalikan channel yang ditutup saat pembatalan) dan juga membawa deadline serta alasan berhenti. Halaman context membahasnya.

break di dalam select

break di case sebuah select keluar dari select, bukan dari for yang melingkupinya. Ini sumber umum perulangan yang tidak pernah selesai. Pakai return, atau beri label pada loop-nya:

Prioritas antar channel

Karena select memilih secara acak, kamu tidak bisa membuat peringkat case di dalam satu pernyataan. Untuk membuat satu channel selalu menang setiap kali ada isinya, periksa channel itu lebih dulu secara tersendiri:

for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	select {
	case <-ctx.Done():
		return ctx.Err()
	case job := <-jobs:
		handle(job)
	}
}

select pertama langsung kembali kalau pembatalan sudah terjadi. Tanpa itu, aliran job yang terus-menerus bisa tetap memenangkan pilihan acak untuk beberapa saat setelah ctx dibatalkan.

Channel nil menonaktifkan case

Send atau receive pada channel nil tidak pernah siap, jadi case pada channel nil praktis dimatikan. Mengisi input yang sudah ditutup dengan nil adalah cara berhenti melakukan select padanya sambil tetap melanjutkan yang lain; halaman channel menunjukkan loop merge yang dibangun dengan cara ini. Trik yang sama bisa menyalakan dan mematikan timeout: biarkan var timeout <-chan time.Time bernilai nil sampai kamu membutuhkannya, lalu assign time.After(d).

Kesalahan umum

  • Mengharapkan urutan sesuai source code. Case yang ditulis pertama tidak diutamakan.
  • Busy loop dengan default. for { select { ... default: } } yang tidak punya pekerjaan akan menghabiskan satu core CPU.
  • Membuat pihak yang kalah bocor. Saat timeout menang, goroutine yang seharusnya mengirim hasil tetap harus bisa selesai. Beri channel-nya buffer berukuran 1.
  • break yang hanya keluar dari select. Pakai label atau return.
  • Satu time.After per iterasi yang dipakai sebagai deadline keseluruhan. Timer itu dimulai ulang di setiap iterasi; buat deadline sekali saja, di luar perulangan.

Pertanyaan yang sering diajukan

Apa fungsi select di Go?

select menunggu sampai salah satu operasi channel-nya (send atau receive) bisa berjalan, lalu menjalankan case itu. Kalau beberapa siap pada saat yang sama, ia memilih salah satu secara acak. Tanpa case default, ia terblokir sampai ada case yang siap; dengan default, ia tidak pernah terblokir.

Bagaimana cara menambahkan timeout pada receive dari channel di Go?

Taruh receive dan sebuah timer di satu select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Mana pun yang terjadi lebih dulu, itulah yang menang. Di dalam perulangan, atau saat pemanggil sudah punya deadline, pakai context.Context dengan context.WithTimeout dan lakukan select pada ctx.Done() sebagai gantinya.

Apakah select di Go memilih case secara berurutan?

Tidak. Saat lebih dari satu case siap, Go memilih secara acak dengan peluang yang sama, sehingga tidak ada case yang bisa membuat case lain kelaparan (starve). Kalau kamu butuh prioritas, periksa channel berprioritas tinggi lebih dulu di select tersendiri dengan default, lalu lanjutkan ke select untuk semua channel.

Kenapa break tidak keluar dari loop for-select saya?

Di dalam select, break hanya keluar dari pernyataan select, bukan dari for yang melingkupinya. Pakai return, atau beri label pada loop-nya (loop: for { select { case <-done: break loop } }).

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai