Channel & select
"Jangan berkomunikasi dengan berbagi memori; berbagilah memori dengan berkomunikasi." Channel adalah bentuk konkret kalimat itu, dan sebagian besar bug channel berasal dari tiga aturan yang tidak diketahui.
Intisari
- Channel tanpa buffer menyinkronkan: pengirim menunggu sampai ada yang menerima.
- Channel berbuffer menampung N nilai sebelum pengirim tertahan.
- Hanya pengirim yang boleh menutup channel, dan hanya kalau pengirimnya satu. Menutup dua kali membuat panik.
- Membaca dari channel tertutup langsung mengembalikan nilai nol; menulis ke channel tertutup membuat panik.
selectmemilih case yang siap; tambahkan<-ctx.Done()supaya tidak pernah menunggu selamanya.
Dua jenis channel
tanpaBuffer := make(chan int) // pengirim menunggu penerima
berbuffer := make(chan int, 10) // pengirim jalan terus sampai 10 nilai menumpuk
berbuffer <- 1 // kirim
v := <-berbuffer // terima
v, ok := <-berbuffer // ok == false kalau channel sudah tertutup DAN kosong
close(berbuffer) // hanya oleh pengirim
| Operasi | Channel nil | Channel terbuka | Channel tertutup |
|---|---|---|---|
| Kirim | Menggantung selamanya | Sukses / menunggu | panic |
| Terima | Menggantung selamanya | Sukses / menunggu | Nilai nol, ok=false |
close | panic | Sukses | panic |
Hafalkan baris "kirim ke channel tertutup = panic". Itu penyebab hampir semua crash yang melibatkan channel, dan ia selalu berarti hal yang sama: ada yang menutup channel dari sisi yang salah.
Siapa yang menutup
Aturannya: channel ditutup oleh pengirim, bukan penerima; dan hanya kalau pengirimnya satu. Penerima tidak perlu menutup apa pun โ channel yang tidak lagi dirujuk siapa pun akan dibersihkan garbage collector seperti nilai biasa.
Kalau ada banyak pengirim, jangan tutup dari sana. Pakai sync.WaitGroup untuk menunggu
semua pengirim selesai, lalu tutup dari satu goroutine koordinator.
// Banyak pengirim, satu penutup
hasil := make(chan Hasil)
var wg sync.WaitGroup
for _, tugas := range daftar {
wg.Add(1)
go func() {
defer wg.Done()
hasil <- kerjakan(tugas)
}()
}
go func() {
wg.Wait()
close(hasil) // satu-satunya penutup, setelah semua pengirim selesai
}()
for h := range hasil { // berhenti sendiri saat channel ditutup
fmt.Println(h)
}
select
select {
case v := <-masuk:
proses(v)
case keluar <- hasil:
// terkirim
case <-time.After(2 * time.Second):
return errors.New("waktu habis")
case <-ctx.Done():
return ctx.Err()
}
select menunggu sampai salah satu case siap. Kalau beberapa siap sekaligus,
satu dipilih acak โ sengaja, supaya tidak ada case yang kelaparan.
Hampir setiap select di kode produksi harus punya
case <-ctx.Done():. Tanpa itu, goroutine-mu tidak punya cara berhenti saat permintaan
dibatalkan atau saat server dimatikan โ dan kamu baru menyadarinya saat deploy menggantung tiga menit
menunggu graceful shutdown yang tidak pernah selesai.
default: jangan menunggu sama sekali
select {
case metrik <- m:
// terkirim
default:
// penuh โ buang saja. Jangan pernah menahan permintaan pengguna
// demi mengirim metrik.
metrikTerbuang.Add(1)
}
Pola yang benar-benar dipakai
Worker pool berbatas
func prosesSemua(ctx context.Context, item []Item, n int) error {
tugas := make(chan Item)
g, ctx := errgroup.WithContext(ctx)
for range n { // n pekerja โ BUKAN satu per item
g.Go(func() error {
for it := range tugas {
if err := proses(ctx, it); err != nil {
return err // membatalkan ctx untuk yang lain
}
}
return nil
})
}
g.Go(func() error {
defer close(tugas)
for _, it := range item {
select {
case tugas <- it:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
return g.Wait()
}
Kenapa jumlah pekerja dibatasi: paralelisme yang tidak dibatasi memindahkan kemacetan ke tempat lain. Seribu goroutine yang masing-masing membuka koneksi database akan menabrak batas koneksi Postgres (biasanya 100) โ dan yang gagal bukan cuma pekerjaan latar, tapi juga permintaan pengguna yang sedang berjalan. Angka pekerja harus lebih kecil dari ukuran connection pool (Fase 4).
Sinyal berhenti dengan channel tertutup
berhenti := make(chan struct{})
go func() {
for {
select {
case <-berhenti:
return
case <-tik.C:
bersihkan()
}
}
}()
close(berhenti) // menutup = memberi sinyal ke SEMUA penerima sekaligus
chan struct{} adalah channel yang tidak membawa data โ hanya sinyal. struct{}
memakai nol byte. Menutupnya membangunkan semua penerima serentak, yang membuatnya jadi cara paling ringkas
menyiarkan "berhenti" ke banyak goroutine sekaligus.
Kapan channel bukan jawabannya
| Kebutuhan | Pakai |
|---|---|
| Melindungi satu variabel bersama | sync.Mutex โ lebih sederhana dan lebih cepat |
| Menghitung sesuatu dari banyak goroutine | sync/atomic |
| Cache dibaca banyak, ditulis jarang | sync.RWMutex |
| Menunggu sekelompok pekerjaan selesai | sync.WaitGroup / errgroup |
| Memindahkan data antar tahap pipeline | Channel |
| Menyiarkan sinyal berhenti | Channel yang ditutup, atau context |
Kesalahan pendatang yang paling umum: memakai channel untuk semuanya karena ia terasa "cara Go". Slogan resminya justru punya lanjutan yang jarang dikutip โ bahwa mutex sepenuhnya sah untuk melindungi keadaan bersama yang sederhana. Kalau sebuah pola dengan channel butuh diagram untuk dijelaskan, mutex hampir selalu lebih baik.
Deadlock
func main() {
ch := make(chan int)
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
}
Runtime Go mendeteksi kasus di mana semua goroutine tertidur dan mematikan program dengan pesan
yang jelas. Yang tidak terdeteksi: deadlock parsial, di mana sebagian goroutine masih berjalan. Untuk itu,
alatnya go test -race dan profil goroutine.
Latihan: tulis pipeline tiga tahap โ pembangkit angka โ penyaring bilangan prima โ pencetak โ
yang dihubungkan channel dan berhenti rapi saat ctx dibatalkan setelah dua detik. Lalu
sengaja tutup channel dari sisi penerima dan baca pesan paniknya. Terakhir, hapus
case <-ctx.Done() dan lihat program menggantung โ itu bentuk bug yang akan kamu temui
lagi di Fase 3.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.