BAB 25: Select — Menunggu Banyak Channel
Pelajari select untuk multiplexing channel: tunggu data dari beberapa sumber, implementasikan timeout, dan kendalikan goroutine yang berjalan lama.
Di bab sebelumnya, kamu menggunakan select sekali — untuk mengecek apakah buffer channel masih punya ruang tanpa blocking. Tapi itu hanya sepersekian dari kemampuan select yang sesungguhnya. Dalam sistem yang nyata, sebuah goroutine sering perlu mendengarkan lebih dari satu channel sekaligus: bisa jadi laporan dari divisi keuangan atau dari divisi operasional, mana yang datang lebih dulu itulah yang diproses. select adalah konstruksi Go yang dirancang tepat untuk situasi ini.
Cara Kerja Select
select mirip switch, tapi setiap case-nya adalah operasi channel — bukan ekspresi nilai. Ia menunggu sampai salah satu channel siap, lalu mengeksekusi case yang bersangkutan.
// Ilustrasi: select menunggu dari dua channel
//
// keuanganCh ──────────┐
// ├──→ select ──→ eksekusi case yang siap
// operasionalCh ────────┘
//
// ┌─────────────────────────────────────────────────┐
// │ select { │
// │ case data := <-keuanganCh: ← siap? jalankan│
// │ case data := <-operasionalCh: ← siap? jalankan│
// │ } │
// │ │
// │ Jika keduanya siap bersamaan → dipilih acak │
// │ Jika tidak ada yang siap → tunggu (block) │
// └─────────────────────────────────────────────────┘
// main.go
package main
import (
"fmt"
"time"
)
func kirimLaporan(ch chan string, isi string, tunda time.Duration) {
time.Sleep(tunda)
ch <- isi
}
func main() {
keuanganCh := make(chan string)
operasionalCh := make(chan string)
go kirimLaporan(keuanganCh, "rekapitulasi Q1 keuangan", 200*time.Millisecond)
go kirimLaporan(operasionalCh, "status gudang operasional", 100*time.Millisecond)
select {
case lap := <-keuanganCh:
fmt.Println("dari keuangan:", lap)
case lap := <-operasionalCh:
fmt.Println("dari operasional:", lap)
}
}
dari operasional: status gudang operasional
Goroutine operasional selesai lebih cepat (100ms vs 200ms), sehingga operasionalCh siap lebih dulu. select langsung mengeksekusi case itu dan tidak menunggu keuanganCh. Begitu satu case terpilih, select selesai — seperti switch yang hanya menjalankan satu branch.
Jika beberapa channel siap pada saat bersamaan, Go memilih salah satunya secara acak. Ini disengaja — tidak ada channel yang diprioritaskan atas channel lain secara default.
Select dalam Loop
Satu select hanya menangkap satu kejadian. Untuk terus mendengarkan channel sampai semua data habis, bungkus select dalam for.
// main.go
package main
import (
"fmt"
"sync"
)
func kumpulkanData(label string, data []int, ch chan string, wg *sync.WaitGroup) {
defer wg.Done()
for _, d := range data {
ch <- fmt.Sprintf("[%s] item-%d", label, d)
}
}
func main() {
keuanganCh := make(chan string, 5)
logistikCh := make(chan string, 5)
var wg sync.WaitGroup
wg.Add(2)
go kumpulkanData("keuangan", []int{101, 102, 103}, keuanganCh, &wg)
go kumpulkanData("logistik", []int{201, 202}, logistikCh, &wg)
go func() {
wg.Wait()
close(keuanganCh)
close(logistikCh)
}()
for {
select {
case lap, ok := <-keuanganCh:
if !ok {
keuanganCh = nil // nonaktifkan case ini
continue
}
fmt.Println("terima:", lap)
case lap, ok := <-logistikCh:
if !ok {
logistikCh = nil
continue
}
fmt.Println("terima:", lap)
}
if keuanganCh == nil && logistikCh == nil {
break
}
}
fmt.Println("semua data terkumpul")
}
terima: [logistik] item-201
terima: [keuangan] item-101
terima: [keuangan] item-102
terima: [logistik] item-202
terima: [keuangan] item-103
semua data terkumpul
Ada teknik penting di sini: ketika channel ditutup dan ok bernilai false, channel di-set ke nil. Channel nil tidak pernah siap untuk operasi apapun, sehingga case yang merujuknya akan dilewati secara permanen oleh select. Ini cara idiomatik untuk “menonaktifkan” case tanpa keluar dari loop lebih awal.
Timeout dengan time.After
Salah satu pola paling berguna dengan select adalah timeout — membatasi berapa lama program mau menunggu data dari sebuah channel.
// Ilustrasi: select dengan timeout
//
// dataCh ──────────────────────────────────────────────┐
// ├──→ select
// time.After(2*second) ────────────────────────────────┘
//
// Mana yang lebih dulu:
// - dataCh siap → proses data
// - timer habis → jalankan case timeout
// main.go
package main
import (
"fmt"
"time"
)
func ambilLaporanEksternal(ch chan string) {
// simulasi sumber data lambat (3 detik)
time.Sleep(3 * time.Second)
ch <- "laporan vendor Q1"
}
func main() {
eksternalCh := make(chan string, 1)
go ambilLaporanEksternal(eksternalCh)
select {
case lap := <-eksternalCh:
fmt.Println("laporan diterima:", lap)
case <-time.After(2 * time.Second):
fmt.Println("timeout: sumber eksternal tidak merespons dalam 2 detik")
fmt.Println("lanjut dengan data cache")
}
}
timeout: sumber eksternal tidak merespons dalam 2 detik
lanjut dengan data cache
time.After mengembalikan channel yang akan menerima satu nilai setelah durasi yang ditentukan. Di select, ini bersaing dengan channel data biasa — mana yang lebih dulu siap, itulah yang dieksekusi.
Jangan gunakan time.After di dalam loop yang berjalan lama. Setiap pemanggilan time.After membuat timer baru yang tidak akan di-garbage collect sampai timer itu menyala. Dalam loop ketat, ini bisa menyebabkan kebocoran memori. Gunakan time.NewTimer dan reset secara manual jika perlu timeout berulang.
Select dengan Default
default dalam select bekerja persis seperti yang kamu lihat di Bab 24: jika tidak ada channel yang siap saat itu, blok default langsung dieksekusi tanpa menunggu.
// main.go
package main
import (
"fmt"
"time"
)
func main() {
pembaruanCh := make(chan string, 1)
// simulasi: data belum tersedia saat pertama dicek
go func() {
time.Sleep(500 * time.Millisecond)
pembaruanCh <- "pembaruan kurs mata uang"
}()
for i := 1; i <= 5; i++ {
select {
case update := <-pembaruanCh:
fmt.Printf("iterasi %d: data masuk — %s\n", i, update)
return
default:
fmt.Printf("iterasi %d: belum ada data, cek lagi nanti\n", i)
}
time.Sleep(150 * time.Millisecond)
}
fmt.Println("selesai polling")
}
iterasi 1: belum ada data, cek lagi nanti
iterasi 2: belum ada data, cek lagi nanti
iterasi 3: belum ada data, cek lagi nanti
iterasi 4: data masuk — pembaruan kurs mata uang
Ini adalah pola polling — program mengecek ketersediaan data secara berkala tanpa menganggur sepenuhnya. Pola ini berguna ketika ada pekerjaan lain yang bisa dilakukan sambil menunggu, atau ketika jumlah iterasi perlu dibatasi.
Fan-In: Menggabungkan Banyak Channel
select juga menjadi fondasi pola fan-in — mengumpulkan output dari banyak sumber concurrent ke satu channel tunggal.
// Ilustrasi: fan-in dari tiga sumber
//
// sumberA ─────────────────────────────────────┐
// sumberB ──────────────────────────────────┐ │
// sumberC ───────────────────────────────┐ │ │
// ↓ ↓ ↓
// ┌────────────────┐
// │ fanIn() │
// │ select loop │
// └───────┬────────┘
// ↓
// outputCh
// (satu stream terpadu)
// main.go
package main
import (
"fmt"
"sync"
"time"
)
func sumberDivisi(nama string, data []string, wg *sync.WaitGroup) <-chan string {
ch := make(chan string, len(data))
wg.Add(1)
go func() {
defer wg.Done()
for _, d := range data {
time.Sleep(50 * time.Millisecond)
ch <- fmt.Sprintf("[%s] %s", nama, d)
}
close(ch)
}()
return ch
}
func fanIn(channels ...<-chan string) <-chan string {
output := make(chan string, 10)
var wg sync.WaitGroup
for _, ch := range channels {
ch := ch // capture loop variable
wg.Add(1)
go func() {
defer wg.Done()
for v := range ch {
output <- v
}
}()
}
go func() {
wg.Wait()
close(output)
}()
return output
}
func main() {
var wg sync.WaitGroup
keuangan := sumberDivisi("keuangan", []string{"invoice-01", "invoice-02"}, &wg)
sdm := sumberDivisi("sdm", []string{"rekap-absen", "lembur-maret"}, &wg)
it := sumberDivisi("it", []string{"tiket-001", "tiket-002", "tiket-003"}, &wg)
terpadu := fanIn(keuangan, sdm, it)
for laporan := range terpadu {
fmt.Println(laporan)
}
wg.Wait()
}
[sdm] rekap-absen
[keuangan] invoice-01
[it] tiket-001
[keuangan] invoice-02
[sdm] lembur-maret
[it] tiket-002
[it] tiket-003
fanIn meluncurkan satu goroutine per channel input, masing-masing meneruskan data ke output. Hasilnya: consumer hanya perlu membaca dari satu channel, sementara semua sumber berjalan concurrent di belakang layar.
Latihan
Latihan 1 — Dua sumber, satu penerima:
Buat dua goroutine yang masing-masing mengirim lima angka ke channel berbeda dengan jeda acak (time.Sleep dengan nilai berbeda). Gunakan select dalam loop untuk menerima dari keduanya dan hitung total semua angka yang diterima. Cetak total setelah semua data masuk.
Latihan 2 — Timeout per operasi:
Buat fungsi ambilData(id int) <-chan string yang mengembalikan channel. Goroutine di dalamnya mensimulasikan kerja dengan time.Sleep menggunakan durasi acak antara 100–500ms. Di main, panggil fungsi ini untuk 5 ID berbeda, lalu gunakan select dengan timeout 300ms untuk setiap channel. Jika data datang sebelum timeout, cetak hasilnya. Jika tidak, cetak "timeout untuk ID N".
Latihan 3 — Fan-in dengan pembatalan:
Kembangkan fungsi fanIn di atas agar menerima parameter done <-chan struct{} tambahan. Jika done ditutup, fanIn harus menghentikan pengumpulan data dan menutup output. Di main, tutup done setelah 200ms untuk mensimulasikan pembatalan di tengah jalan. Petunjuk: tambahkan case <-done: return di dalam goroutine tiap-tiap sumber.
select adalah tool terakhir yang melengkapi pemahaman concurrency Go yang sudah kamu bangun dari goroutine, WaitGroup, Mutex, channel, hingga buffered channel. Bersama-sama, semua kapabilitas ini memberi kamu kemampuan untuk merancang sistem yang memanfaatkan paralelisme secara aman, efisien, dan ekspresif — persis seperti yang digunakan di infrastruktur skala besar yang dibangun dengan Go.