BAB 26: Channel Direction, Range, dan Close
Kuasai channel direction untuk keamanan tipe, idiom range pada channel, dan aturan menutup channel yang benar di Go.
Sepanjang bab-bab sebelumnya, kamu sudah menggunakan channel direction — notasi <-chan dan chan<- — di beberapa tempat: pada parameter fungsi worker pool di Bab 24 dan fungsi sumberDivisi di Bab 25. Notasi itu selalu hadir tanpa penjelasan mendalam, karena konteksnya saat itu berfokus pada pola yang lebih besar. Sekarang saatnya membahas dua fitur ini secara sungguh-sungguh: mengapa direction penting untuk keamanan kode, bagaimana for range pada channel bekerja di balik layar, dan aturan close yang tidak boleh dilanggar.
Channel Direction: Kontrak Lewat Tipe
Secara default, channel yang dibuat dengan make bersifat bidirectional — bisa digunakan untuk kirim maupun terima. Tapi saat channel diteruskan ke fungsi, kamu bisa mempersempit aksesnya hanya ke satu arah.
// Tiga jenis channel berdasarkan arah
//
// chan string ─── bidirectional (kirim DAN terima)
// chan<- string ─── send-only (kirim SAJA)
// <-chan string ─── receive-only (terima SAJA)
//
// Cara membaca anotasi:
// chan<- string → panah masuk ke chan = data masuk = kirim
// <-chan string → panah keluar dari chan = data keluar = terima
Go secara otomatis mengkonversi channel bidirectional ke directional saat dilempar ke fungsi. Yang tidak bisa dilakukan adalah kebalikannya — channel directional tidak bisa dikembalikan ke bidirectional.
// main.go
package main
import "fmt"
// hanya boleh kirim ke channel ini
func muatAntrian(ch chan<- string, items []string) {
for _, item := range items {
ch <- item
}
close(ch) // hanya sender yang boleh menutup
}
// hanya boleh terima dari channel ini
func prosesAntrian(ch <-chan string) {
for item := range ch {
fmt.Printf("memproses: %s\n", item)
}
}
func main() {
antrianCh := make(chan string, 5) // bidirectional
go muatAntrian(antrianCh, []string{
"rekap-januari", "rekap-februari", "rekap-maret",
})
prosesAntrian(antrianCh) // otomatis dikonversi ke <-chan string
}
memproses: rekap-januari
memproses: rekap-februari
memproses: rekap-maret
muatAntrian menerima chan<- string — ia hanya bisa mengirim. Jika ada yang secara tidak sengaja menulis <-ch di dalam fungsi itu, compiler akan langsung menolak dengan error. Sama halnya dengan prosesAntrian yang menerima <-chan string — tidak ada cara untuk mengirim data dari sana.
Gunakan channel direction di semua signature fungsi yang berurusan dengan channel. Ini membuat niat kode terlihat jelas dari signature saja, tanpa perlu membaca isi fungsi. Compiler menjadi penjaga pertama yang menangkap kesalahan arah sebelum program dijalankan.
Mengembalikan Channel dari Fungsi
Pola yang sangat idiomatik di Go adalah fungsi yang mengembalikan channel sebagai nilai balik. Dengan direction, kamu bisa memastikan caller hanya bisa membaca dari channel tersebut, tidak mengirim ke dalamnya.
// main.go
package main
import (
"fmt"
"time"
)
// mengembalikan receive-only channel — caller hanya bisa membaca
func generateNomorTiket(awal, akhir int) <-chan int {
tiketCh := make(chan int)
go func() {
for nomor := awal; nomor <= akhir; nomor++ {
time.Sleep(30 * time.Millisecond)
tiketCh <- nomor
}
close(tiketCh)
}()
return tiketCh // bidirectional dalam, receive-only keluar
}
func main() {
tiketCh := generateNomorTiket(1001, 1007)
for nomor := range tiketCh {
fmt.Printf("melayani tiket #%d\n", nomor)
}
fmt.Println("semua tiket selesai dilayani")
}
melayani tiket #1001
melayani tiket #1002
melayani tiket #1003
melayani tiket #1004
melayani tiket #1005
melayani tiket #1006
melayani tiket #1007
semua tiket selesai dilayani
Di dalam generateNomorTiket, channel dibuat sebagai chan int (bidirectional) karena goroutine anonim perlu mengirim ke dalamnya dan menutupnya. Di nilai balik, tipenya dideklarasikan sebagai <-chan int — Go otomatis mengkonversinya. Caller di main tidak punya akses untuk mengirim ke channel itu, apalagi menutupnya.
Range pada Channel: Cara Kerjanya
for item := range ch adalah gula sintaks yang menyederhanakan pola penerimaan dua-nilai. Di baliknya, Go terus-menerus melakukan item, ok := <-ch sampai ok bernilai false.
// Equivalensi: for range vs manual
//
// for item := range ch { for {
// // proses item item, ok := <-ch
// } ≡ if !ok {
// break
// }
// // proses item
// }
Perbedaan penting: for range hanya berhenti saat channel ditutup. Jika channel tidak pernah ditutup, loop ini akan berjalan selamanya — goroutine yang menjalankannya akan menggantung dan tidak pernah selesai. Ini adalah sumber goroutine leak yang paling umum.
// main.go
package main
import (
"fmt"
"sync"
)
func kumpulkanMetrik(sumber string, data []float64, ch chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
for _, nilai := range data {
ch <- fmt.Sprintf("%s: %.2f", sumber, nilai)
}
// tidak memanggil close(ch) di sini karena
// banyak sender berbagi satu channel yang sama
}
func main() {
metrikCh := make(chan string, 10)
var wg sync.WaitGroup
sumber := map[string][]float64{
"cpu": {72.5, 68.3, 80.1},
"memori": {45.2, 47.8, 46.0},
"disk": {12.4, 13.1},
}
for nama, data := range sumber {
wg.Add(1)
go kumpulkanMetrik(nama, data, metrikCh, &wg)
}
// tutup channel setelah semua sender selesai
go func() {
wg.Wait()
close(metrikCh)
}()
for metrik := range metrikCh {
fmt.Println(metrik)
}
fmt.Println("semua metrik terkumpul")
}
memori: 45.20
cpu: 72.50
disk: 12.40
cpu: 68.30
memori: 47.80
disk: 13.10
cpu: 80.10
memori: 46.00
semua metrik terkumpul
Pola ini — banyak sender, satu channel, close ditangani goroutine koordinator — adalah cara yang benar untuk menangani situasi di mana menutup channel dari sender langsung tidak aman karena ada sender lain yang masih aktif.
Aturan Close yang Tidak Boleh Dilanggar
close pada channel terlihat sederhana, tapi ada tiga aturan yang jika dilanggar akan menyebabkan panic saat runtime — bukan compile time.
// Tiga aturan close:
//
// 1. Hanya sender yang boleh menutup channel
// ┌─────────────────────────────────────┐
// │ sender ──→ ch ──→ receiver │
// │ ↑ │
// │ close() hanya dari sini │
// └─────────────────────────────────────┘
//
// 2. Jangan close channel yang sudah di-close
// close(ch) → ok
// close(ch) → PANIC: close of closed channel
//
// 3. Jangan kirim ke channel yang sudah di-close
// close(ch) → ok
// ch <- data → PANIC: send on closed channel
Penerimaan dari channel yang sudah ditutup tidak panic — ia mengembalikan zero value dari tipe channel tersebut, dengan ok bernilai false. Inilah kenapa for range bisa berhenti dengan bersih.
Menutup channel dari sisi receiver adalah kesalahan desain yang hampir pasti menyebabkan panic. Jika receiver perlu memberi sinyal bahwa ia tidak mau menerima data lagi, gunakan channel sinyal terpisah (done chan struct{}) yang dibaca oleh sender — bukan menutup channel data secara langsung.
Membaca Setelah Close
Satu perilaku yang sering tidak disadari: channel yang sudah ditutup masih bisa dibaca. Data yang tersisa di buffer (jika buffered) tetap bisa diambil. Setelah buffer habis, penerimaan menghasilkan zero value dengan ok = false.
// main.go
package main
import "fmt"
func main() {
arsipCh := make(chan string, 3)
arsipCh <- "dokumen-2023-Q4"
arsipCh <- "dokumen-2024-Q1"
arsipCh <- "dokumen-2024-Q2"
close(arsipCh) // tutup setelah mengisi buffer
// masih bisa baca semua yang tersisa
for i := 0; i < 5; i++ {
doc, ok := <-arsipCh
if ok {
fmt.Printf("dokumen ditemukan: %s\n", doc)
} else {
fmt.Printf("iterasi %d: channel kosong dan tertutup\n", i+1)
}
}
}
dokumen ditemukan: dokumen-2023-Q4
dokumen ditemukan: dokumen-2024-Q1
dokumen ditemukan: dokumen-2024-Q2
iterasi 4: channel kosong dan tertutup
iterasi 5: channel kosong dan tertutup
Tiga item di buffer berhasil dibaca. Iterasi keempat dan kelima menghasilkan string kosong ("") dengan ok = false. Ini menjelaskan kenapa for range aman — ia berhenti tepat setelah item terakhir yang valid, tanpa menghasilkan zero value palsu.
Latihan
Latihan 1 — Pipeline tiga fungsi:
Buat tiga fungsi: hasilkanID() <-chan int yang menghasilkan ID 1–8, formatID(in <-chan int) <-chan string yang mengubah setiap ID menjadi string "REQ-XXXX", dan cetak(in <-chan string) yang mencetak setiap string. Hubungkan ketiganya dalam main sebagai pipeline. Setiap fungsi harus menutup channel outputnya saat selesai.
Latihan 2 — Banyak sender, satu range:
Buat 5 goroutine yang masing-masing mengirim 3 string ke satu buffered channel. Gunakan sync.WaitGroup dan goroutine koordinator untuk menutup channel setelah semua sender selesai. Di main, gunakan for range untuk menerima semua 15 string.
Latihan 3 — Generator dengan pembatalan:
Modifikasi fungsi generateNomorTiket dari contoh di atas agar menerima parameter stop <-chan struct{}. Goroutine di dalamnya harus berhenti menghasilkan nomor jika stop ditutup, lalu menutup channel tiket. Di main, tutup stop setelah menerima 4 nomor pertama dan verifikasi bahwa program berhenti dengan bersih tanpa goroutine leak.
Dengan channel direction, idiom for range, dan aturan close yang kamu kuasai sekarang, kamu punya pemahaman yang lengkap tentang cara Go mengelola komunikasi antar goroutine — dari yang paling sederhana hingga sistem pipeline berlapis. Fondasi concurrency Go yang solid ini membuka pintu ke topik yang lebih tinggi: context untuk pembatalan terstruktur, errgroup untuk mengelola error dari banyak goroutine, dan pola-pola lanjutan yang dipakai di codebase produksi skala besar.